File size: 44,620 Bytes
b6b6d72 aa26808 3e8123c aa26808 3e8123c aa26808 3e8123c aa26808 3e8123c aa26808 b6b6d72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lit Template security check — check template bindings in your code</title>
<meta name="description" content="This free browser tool finds Lit and lit-html template bindings that auto-escaping does not cover and names a safe form.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/lit-binding-xss-audit"></head>
<body>
<div class="wrap">
<h1>Lit Template security check — check template bindings in your code</h1>
<p class="lede">Finds the Lit / lit-html template bindings that auto-escaping does not cover - unsafeHTML, javascript: href, .innerHTML, srcdoc, unsafeStatic - and names the safe form. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">import { LitElement, html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { html as staticHtml, unsafeStatic } from 'lit/static-html.js';
export class TicketCard extends LitElement {
static properties = {
note: { type: String },
badge: { type: String },
docsUrl: { type: String },
tag: { type: String },
accent: { type: String },
preview: { type: String }
};
render() {
const tag = unsafeStatic(this.tag);
return html`
<style>
.card { border-left: 4px solid ${this.accent}; }
</style>
<article class="card">
<h2>${this.title}</h2>
<div class="note">${unsafeHTML(this.note)}</div>
<div class="badge">${unsafeSVG(this.badge)}</div>
<a href="${this.docsUrl}">Open the ticket</a>
<a href="javascript:void(0)">Print</a>
<button onclick="${this.handler}">Assign</button>
<div .innerHTML=${this.preview}></div>
<iframe srcdoc="${this.preview}"></iframe>
${staticHtml`<${tag}>${this.note}</${tag}>`}
<script>
window.__ticket = "${this.title}";
</script>
</article>
`;
}
firstUpdated() {
this.renderRoot.querySelector('.note').innerHTML = this.note;
this.renderRoot.querySelector('.card').insertAdjacentHTML('beforeend', this.badge);
document.write('<!-- ticket ' + this.title + ' -->');
}
}
customElements.define('ticket-card', TicketCard);</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Audit the Lit component file you have open and get every unsafe binding with its line number and the safe form to write instead.. <strong>In the editor:</strong> Scan every component in the workspace and export one dated evidence report listing each unsafe binding, file and line. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var LITBIND_RULES = [
{"id":"unsafe-html","kind":"line","sev":"high","re":"unsafeHTML\\s*\\(",
"msg":"unsafeHTML() hands the string to the HTML parser - lit-html does not escape it.",
"fix":"Drop unsafeHTML and bind the value as text, or run it through DOMPurify.sanitize() first."},
{"id":"unsafe-svg","kind":"line","sev":"high","re":"unsafeSVG\\s*\\(",
"msg":"unsafeSVG() parses the string as SVG markup, which can carry script and event attributes.",
"fix":"Render the shape from data with svg`...` instead of injecting markup."},
{"id":"unsafe-static","kind":"line","sev":"high","re":"unsafeStatic\\s*\\(",
"msg":"unsafeStatic() splices the value into the template text before it is parsed and cached.",
"fix":"Use literal`...` values you wrote yourself, never a runtime string."},
{"id":"javascript-url","kind":"line","sev":"high","re":"(?:href|src|xlink:href)\\s*=\\s*[\"']?\\s*javascript:",
"msg":"A javascript: URL in a template attribute runs on click.",
"fix":"Use a normal https: URL and move the behaviour to an @click handler."},
{"id":"url-attr-binding","kind":"line","sev":"med","capture":1,
"re":"(?:href|src|action|formaction|xlink:href)\\s*=\\s*[\"']?\\$\\{([^}]*)\\}",
"unless":"safeUrl|sanitizeUrl|toHttpUrl|new URL|allowedUrl|ifDefined\\s*\\(\\s*(?:safeUrl|sanitizeUrl)",
"msg":"A URL attribute bound straight from data - lit escapes the characters but keeps the scheme.",
"fix":"Pass it through a helper that accepts only http: and https: before binding."},
{"id":"innerhtml-binding","kind":"line","sev":"high","re":"\\.innerHTML\\s*=\\s*\\$\\{",
"msg":"A .innerHTML property binding writes raw markup into the element.",
"fix":"Bind the value as template text, or set .textContent instead."},
{"id":"innerhtml-assign","kind":"line","sev":"high","re":"\\.innerHTML\\s*=(?!=)","skip_if":"innerhtml-binding",
"msg":"Assigning .innerHTML in component code bypasses the template and its escaping.",
"fix":"Render through this.render() / html`...` or assign .textContent."},
{"id":"srcdoc-binding","kind":"line","sev":"high","re":"srcdoc\\s*=\\s*[\"']?\\$\\{",
"msg":"srcdoc is a document body - a binding here becomes markup inside the iframe.",
"fix":"Point the iframe at a src URL you control, or sandbox it and post the data in."},
{"id":"inline-event-attr","kind":"line","sev":"high","re":"\\bon[a-z]{3,12}\\s*=\\s*[\"']?\\$\\{",
"msg":"An on* attribute binding is compiled as an inline handler string, not a listener.",
"fix":"Use the lit event binding @click=${this.onClick} with a function reference."},
{"id":"binding-in-style","kind":"style","sev":"high",
"msg":"A binding inside \u003cstyle\u003e lands in CSS text, where lit-html applies no escaping.",
"fix":"Move the value to a bound style property or a CSS custom property on the host."},
{"id":"binding-in-script","kind":"script","sev":"high",
"msg":"A binding inside \u003cscript\u003e becomes JavaScript source at parse time.",
"fix":"Delete the inline script and read the value from a property in the component class."},
{"id":"document-write","kind":"line","sev":"high","re":"document\\.write(?:ln)?\\s*\\(",
"msg":"document.write() parses its argument as markup in the page document.",
"fix":"Render the content through the component template instead."},
{"id":"insert-adjacent-html","kind":"line","sev":"med","re":"insertAdjacentHTML\\s*\\(",
"msg":"insertAdjacentHTML() inserts parsed markup next to the element.",
"fix":"Create the node and set .textContent, or render it from the template."},
{"id":"sanitizer-missing","kind":"file","sev":"high",
"needs":"unsafeHTML\\s*\\(|unsafeSVG\\s*\\(|unsafeStatic\\s*\\(",
"unless":"DOMPurify|sanitizeHtml|sanitizeHTML|trustedTypes|createPolicy",
"msg":"This file injects markup but never calls a sanitizer, so nothing narrows what the parser accepts.",
"fix":"Add DOMPurify.sanitize() at the injection point or remove the unsafe directive."},
{"id":"cra-report-window","kind":"date","sev":"info","since":"2026-09-11",
"msg":"EU CRA Article 14 reporting is live: an actively exploited vulnerability in a shipped product needs an early warning within 24 hours, a notification within 72 hours and a final report within 14 days.",
"fix":"Close the bindings above before the component ships, so the clock never starts."}
]
;</script>
<script>/* Lit Template Binding Audit - one brain, used by the extension and by the web page. */
(function (root) {
'use strict';
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : root.LITBIND_RULES;
function rx(pattern, flags) { return new RegExp(pattern, flags || ''); }
function lineRules() {
return RULES.filter(function (r) { return r.kind === 'line'; });
}
function check(text, opts) {
opts = opts || {};
var today = opts.today || '';
var lines = String(text == null ? '' : text).split(/\r?\n/);
var findings = [];
var fired = {};
var inStyle = false;
var inScript = false;
var styleRule = RULES.filter(function (r) { return r.kind === 'style'; })[0];
var scriptRule = RULES.filter(function (r) { return r.kind === 'script'; })[0];
var line = lineRules();
for (var i = 0; i < lines.length; i++) {
var src = lines[i];
var no = i + 1;
var seen = {};
var opensStyle = /<style[\s>]/i.test(src);
var opensScript = /<script[\s>]/i.test(src);
if ((inStyle || opensStyle) && !/<style[^>]*>[^<]*<\/style>/i.test(src) && src.indexOf('${') !== -1 && styleRule) {
findings.push(mk(styleRule, no, src));
seen[styleRule.id] = 1;
}
if ((inScript || opensScript) && src.indexOf('${') !== -1 && scriptRule) {
findings.push(mk(scriptRule, no, src));
seen[scriptRule.id] = 1;
}
if (opensStyle) { inStyle = true; }
if (/<\/style>/i.test(src)) { inStyle = false; }
if (opensScript) { inScript = true; }
if (/<\/script>/i.test(src)) { inScript = false; }
for (var j = 0; j < line.length; j++) {
var r = line[j];
if (r.skip_if && seen[r.skip_if]) { continue; }
var m = rx(r.re, 'i').exec(src);
if (!m) { continue; }
if (r.unless) {
var probe = r.capture ? (m[r.capture] || '') : src;
if (rx(r.unless, 'i').test(probe)) { continue; }
}
seen[r.id] = 1;
fired[r.id] = fired[r.id] || no;
findings.push(mk(r, no, src));
}
if (seen[styleRule && styleRule.id]) { fired[styleRule.id] = fired[styleRule.id] || no; }
if (seen[scriptRule && scriptRule.id]) { fired[scriptRule.id] = fired[scriptRule.id] || no; }
}
RULES.filter(function (r) { return r.kind === 'file'; }).forEach(function (r) {
var need = rx(r.needs, 'i');
var at = 0;
for (var i = 0; i < lines.length; i++) {
if (need.test(lines[i])) { at = i + 1; break; }
}
if (!at) { return; }
if (r.unless && rx(r.unless, 'i').test(text)) { return; }
findings.push(mk(r, at, lines[at - 1]));
});
var high = findings.filter(function (f) { return f.sev === 'high'; });
RULES.filter(function (r) { return r.kind === 'date'; }).forEach(function (r) {
if (!high.length) { return; }
if (!today || today < r.since) { return; }
findings.push(mk(r, high[0].line, ''));
});
findings.sort(function (a, b) { return a.line - b.line; });
return { findings: findings };
}
function mk(rule, line, src) {
return {
check: rule.id,
sev: rule.sev,
msg: rule.msg,
fix: rule.fix,
line: line,
snippet: String(src || '').trim().slice(0, 120)
};
}
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
root.LITBINDENGINE = api;
if (typeof module !== 'undefined') { module.exports = api; }
})(typeof window !== 'undefined' ? window : globalThis);
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var LITBINDREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = LITBINDREPORT;
else window.LITBINDREPORT = LITBINDREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.LITBINDENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.LITBINDREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/lit-binding-xss-audit" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'lit-binding-xss-audit',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/lit-binding-xss-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'lit-binding-xss-audit',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/lit-binding-xss-audit" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'lit-binding-xss-audit',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/lit-binding-xss-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'lit-binding-xss-audit',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/lit-binding-xss-audit <file>
npx @readystack/lit-binding-xss-audit --mcp
docker run --rm -v "$PWD:/w" getreadystack/lit-binding-xss-audit /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 15 rules Lit Template Binding Audit checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"lit-binding-xss-audit",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Finds the lit-html binding positions auto-escaping does not cover, before the component ships</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">An application-security contractor reviewing component code bills roughly 150 to 250 an hour; this audit runs on the file you have open.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_rQuve6vrYJ7scHEYwBnxBPid2txKeXvLSITuq0H0N0t" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-17">Updated 2026-09-17</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-17","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku99611_result_card.jpg" alt="Lit Template Binding Audit" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Eight binding positions in one 46-line Lit component ship markup that lit-html never escapes - and if you maintain lit-html web components for a product sold in the EU, those eight are yours to close before anyone starts counting hours.</p>
<p>Here is the measurement. The audit ships two fixtures: a <code>ticket-card</code> component written the way component code drifts, and the clean rewrite of the same component. The dirty one raises 15 findings - 12 high, 2 medium, 1 informational - which is every rule the audit has. The clean rewrite of the same component raises zero. The rules are line-addressed patterns over a rule file you can read.</p>
<p>The reason there is anything to find is that lit-html's escaping is real but narrow. A value dropped into a child position is escaped. A value that reaches the parser by another route is not, and a Lit template has more routes than most people hold in their head at once:</p>
<pre><code>html`
<style>.card { border-left: 4px solid ${this.accent}; }</style>
<div class="note">${unsafeHTML(this.note)}</div>
<a href="${this.docsUrl}">Open the ticket</a>
<div .innerHTML=${this.preview}></div>
<iframe srcdoc="${this.preview}"></iframe>
<button onclick="${this.handler}">Assign</button>
`</code></pre>
<p>Every line above is a finding. The binding inside <code><style></code> lands in CSS text, where no escaping applies. <code>unsafeHTML</code> hands its string to the HTML parser by design. The <code>href</code> binding is escaped character by character and still keeps whatever scheme the data brought with it, <code>javascript:</code> included. A <code>.innerHTML</code> property binding writes raw markup into the element; <code>srcdoc</code> is a whole document body; and <code>onclick="${...}"</code> is compiled as an inline handler string rather than the listener <code>@click=${this.assign}</code> would have given you.</p>
<p>The safe forms are unremarkable once they are named: a CSS custom property on the host instead of a binding in <code><style></code>; the value bound as text instead of <code>unsafeHTML</code>; <code>href=${ifDefined(this.safeUrl(url))}</code> with a helper that accepts only <code>http:</code> and <code>https:</code>; <code><pre>${this.preview}</pre></code> instead of <code>.innerHTML</code>; an iframe <code>src</code> you control instead of <code>srcdoc</code>; <code>@click</code> instead of <code>onclick</code>. That is what the clean fixture does, and why it reports nothing.</p>
<p>There is a clock behind this now. EU Cyber Resilience Act Article 14 reporting has been in force since 11 September 2026: an actively exploited vulnerability in a product with digital elements means an early warning within 24 hours, a notification within 72 hours and a final report within 14 days. An injection in a shipped web component is exactly that shape of defect, which is why the audit adds one informational finding naming those three windows whenever a high finding exists.</p>
<p>A general linter will not do this for you. ESLint reads html<code>...</code> as a plain tagged template literal, so a binding in a style block or an <code>onclick</code> attribute is ordinary text to it. A chatbot can explain lit-html escaping and still cannot hand you a file and a line number.</p>
<p>The audit of the file you have open is free and complete: all 15 rules, every line number, every suggested fix, no watermark and no run limit. The licence buys a different job - one command sweeps every component in the workspace and writes a dated evidence report listing each unsafe binding with its file and line. For scale, an application-security contractor reviewing component code bills roughly 150 to 250 an hour.</p>
<p>The same rule file and the same <code>engine.check()</code> run in the browser page, so you can paste a component in and get the identical findings before you install anything.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku99611_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku99611_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Lit Template Binding Audit actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a Lit or lit-html component file and reports the binding positions where auto-escaping does not apply: unsafeHTML, unsafeSVG, unsafeStatic, a .innerHTML property binding, srcdoc, an on* attribute binding, a URL attribute bound straight from data, and any binding sitting inside a style or script block. Each finding carries a line number and the safe form to write instead.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Developers who ship Lit web components in a product placed on the EU market, and the reviewer who has to sign off on that component. If your team writes lit-html templates and an exploitable injection in one of them would become a CRA Article 14 reporting event, this is aimed at you rather than at a general JavaScript linter user.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why will a free linter or a chatbot not find these?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">ESLint and Prettier read html`...` as a plain tagged template literal, so a binding in a style block or an on* attribute looks like ordinary text to them. A chatbot can explain lit-html escaping but cannot walk your repository line by line and will not tell you which line number in which file ships the hole.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what does the licence add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free is the whole audit of the file you have open: all 15 rules, every finding, every line number, every suggested fix, with no watermark and no run limit. The licence adds a different job: one command sweeps every component in the workspace and writes a dated evidence report listing each unsafe binding with its file and line.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost done by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">An application-security contractor reviewing component code bills roughly 150 to 250 an hour, and a template review of a component library is rarely under a day. The audit here is 15 rules run against the open file in under a second, and the paid sweep produces the written record that the review would otherwise have to type.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Lit Template Binding Audit checks the file you open against 15 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Lit Template Binding Audit actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a Lit or lit-html component file and reports the binding positions where auto-escaping does not apply: unsafeHTML, unsafeSVG, unsafeStatic, a .innerHTML property binding, srcdoc, an on* attribute binding, a URL attribute bound straight from data, and any binding sitting inside a style or script block. Each finding carries a line number and the safe form to write instead."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Developers who ship Lit web components in a product placed on the EU market, and the reviewer who has to sign off on that component. If your team writes lit-html templates and an exploitable injection in one of them would become a CRA Article 14 reporting event, this is aimed at you rather than at a general JavaScript linter user."}}, {"@type": "Question", "name": "Why will a free linter or a chatbot not find these?", "acceptedAnswer": {"@type": "Answer", "text": "ESLint and Prettier read html`...` as a plain tagged template literal, so a binding in a style block or an on* attribute looks like ordinary text to them. A chatbot can explain lit-html escaping but cannot walk your repository line by line and will not tell you which line number in which file ships the hole."}}, {"@type": "Question", "name": "What is free and what does the licence add?", "acceptedAnswer": {"@type": "Answer", "text": "Free is the whole audit of the file you have open: all 15 rules, every finding, every line number, every suggested fix, with no watermark and no run limit. The licence adds a different job: one command sweeps every component in the workspace and writes a dated evidence report listing each unsafe binding with its file and line."}}, {"@type": "Question", "name": "What would this cost done by hand?", "acceptedAnswer": {"@type": "Answer", "text": "An application-security contractor reviewing component code bills roughly 150 to 250 an hour, and a template review of a component library is rarely under a day. The audit here is 15 rules run against the open file in under a second, and the paid sweep produces the written record that the review would otherwise have to type."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Lit Template Binding Audit checks the file you open against 15 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:99611,slug:"lit-binding-xss-audit",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/autorenew-signup-lint"><span class="yj-rel-t">Auto-Renewal Signup Lint (California ARL)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bem-class-extractor-html"><span class="yj-rel-t">BEM Class Auditor for HTML and SCSS</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/eaa-form-lint-wcag22"><span class="yj-rel-t">EAA Form Lint: WCAG 2.2 AA for HTML</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/en301549-lint"><span class="yj-rel-t">EN 301 549 Lint — European Accessibility Act rules, with the clause number, in</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/erb-escape-audit"><span class="yj-rel-t">ERB Escape Audit - Rails XSS lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/mv3-manifest-preflight"><span class="yj-rel-t">MV3 Manifest Preflight - Chrome Web Store</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>
|