{"record":{"id":"04b6800effba3053","repo":"nexu-io/open-design","slug":"srcdoc-attributes-are-not-supported-in-live-artifa","errorCode":null,"errorMessage":"srcdoc attributes are not supported in live artifact previews","messagePattern":"srcdoc attributes are not supported in live artifact previews","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/render.ts","lineNumber":34,"sourceCode":"\nconst TEMPLATE_INTERPOLATION = /{{\\s*([^{}]+?)\\s*}}/g;\nconst RAW_TEMPLATE_INTERPOLATION = /{{{[^{}]*}}}|{{\\s*&[^{}]*}}/;\nconst TEMPLATE_PATH = /^(?:data|[A-Za-z_][A-Za-z0-9_]*)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|\\d+))*$/;\n// `data-od-repeat=\"item in data.items\"` — one loop variable over one `data.*` array.\nconst REPEAT_DIRECTIVE = /\\s*\\bdata-od-repeat\\s*=\\s*\"([^\"]*)\"/i;\nconst REPEAT_DIRECTIVE_SPEC = /^\\s*([A-Za-z_][A-Za-z0-9_]*)\\s+in\\s+(data(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|\\d+))*)\\s*$/;\nconst EXECUTABLE_TEMPLATE_PATTERNS: Array<{ pattern: RegExp; message: string }> = [\n  { pattern: /<\\s*script\\b/i, message: 'script elements are not supported in live artifact previews' },\n  { pattern: /<\\s*iframe\\b/i, message: 'iframe elements are not supported in live artifact previews' },\n  { pattern: /\\bsrcdoc\\s*=/i, message: 'srcdoc attributes are not supported in live artifact previews' },\n  { pattern: /\\son[a-z][a-z0-9_-]*\\s*=/i, message: 'event handler attributes are not supported in live artifact previews' },\n  { pattern: /(?:href|src|action|formaction)\\s*=\\s*['\"]?\\s*javascript\\s*:/i, message: 'javascript: URLs are not supported in live artifact previews' },\n  { pattern: /\\bdata-od-(?:html|raw|bind-html)\\b/i, message: 'raw HTML insertion directives are not supported' },\n];\n\nexport function validateHtmlTemplateV1Security(templateHtml: string): void {\n  for (const { pattern, message } of EXECUTABLE_TEMPLATE_PATTERNS) {\n    if (pattern.test(templateHtml)) throw new Error(message);\n  }\n}\n\nexport function escapeHtmlTemplateValue(value: unknown): string {\n  return String(value)\n    .replaceAll('&', '&amp;')\n    .replaceAll('<', '&lt;')\n    .replaceAll('>', '&gt;')\n    .replaceAll('\"', '&quot;')\n    .replaceAll(\"'\", '&#39;');\n}\n\n/**\n * A binding resolver for one scope. Given a trimmed binding path (e.g.\n * `data.title` or a loop variable path like `item.label`) it returns the\n * already-escaped scalar string to substitute, or throws for an unsupported\n * path. Loop scopes delegate non-matching heads (including `data.*`) to their\n * parent so global bindings keep working inside a repeat.","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/render.ts#L16-L52","documentation":"Thrown by validateHtmlTemplateV1Security when the template matches /\\bsrcdoc\\s*=/i — a srcdoc attribute anywhere in the markup. srcdoc is the inline-HTML delivery channel for iframes and is blocked independently of the iframe tag itself, so that even an iframe-less element carrying srcdoc (or a future element using the attribute) cannot smuggle HTML into a nested browsing context.","triggerScenarios":"Any attribute written as srcdoc=... on any element, including <div srcdoc=...>, <iframe srcdoc='...'>, or quoted/unquoted forms; also matches inside text content if it happens to look like the attribute assignment.","commonSituations":"Model embeds an iframe using srcdoc instead of src to dodge the iframe-element check; developer copies an old embed snippet that used srcdoc; misunderstanding that the filter keys on the attribute, not the element.","solutions":["Remove the srcdoc attribute; render content inline in the template body instead.","If you need nested HTML, it must be authored directly in the template (subject to the same security scan), not passed as a string attribute."],"exampleFix":"// before\n<template><div srcdoc='{{data.html}}'></div></template>\n// after\n<template><div>{{data.html}}</div></template>","handlingStrategy":"validation","validationCode":"function assertNoSrcdoc(html: string): void {\n  if (/\\bsrcdoc\\s*=/i.test(html)) throw new Error('srcdoc attribute not allowed');\n}","typeGuard":"function isSrcdocFree(html: string): boolean {\n  return !/\\bsrcdoc\\s*=/i.test(html);\n}","tryCatchPattern":"try { validateHtmlTemplateV1Security(tpl); } catch (e) { throw e; }","preventionTips":["Render nested HTML inline in the template body, not via srcdoc.","Scan committed templates for the srcdoc attribute in CI.","Remember the filter keys on the attribute, so refactoring the element won't help."],"tags":["security","xss","html-attribute","live-artifacts","validation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}