nexu-io/open-design · error · Error
srcdoc attributes are not supported in live artifact…
Error message
srcdoc attributes are not supported in live artifact previews
What it means
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.
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.
Example fix
// before
<template><div srcdoc='{{data.html}}'></div></template>
// after
<template><div>{{data.html}}</div></template> Defensive patterns
Strategy: validation
Validate before calling
function assertNoSrcdoc(html: string): void {
if (/\bsrcdoc\s*=/i.test(html)) throw new Error('srcdoc attribute not allowed');
} Type guard
function isSrcdocFree(html: string): boolean {
return !/\bsrcdoc\s*=/i.test(html);
} Try / catch
try { validateHtmlTemplateV1Security(tpl); } catch (e) { throw e; } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- event handler attributes are not supported in live artifact…
- iframe elements are not supported in live artifact previews
- javascript: URLs are not supported in live artifact previews
- raw HTML insertion directives are not supported
- raw template interpolation is not supported
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/04b6800effba3053.
Report an issue: GitHub.
Appendix: source
Thrown at apps/daemon/src/live-artifacts/render.ts:34
const TEMPLATE_INTERPOLATION = /{{\s*([^{}]+?)\s*}}/g;
const RAW_TEMPLATE_INTERPOLATION = /{{{[^{}]*}}}|{{\s*&[^{}]*}}/;
const TEMPLATE_PATH = /^(?:data|[A-Za-z_][A-Za-z0-9_]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$/;
// `data-od-repeat="item in data.items"` — one loop variable over one `data.*` array.
const REPEAT_DIRECTIVE = /\s*\bdata-od-repeat\s*=\s*"([^"]*)"/i;
const REPEAT_DIRECTIVE_SPEC = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(data(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*)\s*$/;
const EXECUTABLE_TEMPLATE_PATTERNS: Array<{ pattern: RegExp; message: string }> = [
{ pattern: /<\s*script\b/i, message: 'script elements are not supported in live artifact previews' },
{ pattern: /<\s*iframe\b/i, message: 'iframe elements are not supported in live artifact previews' },
{ pattern: /\bsrcdoc\s*=/i, message: 'srcdoc attributes are not supported in live artifact previews' },
{ pattern: /\son[a-z][a-z0-9_-]*\s*=/i, message: 'event handler attributes are not supported in live artifact previews' },
{ pattern: /(?:href|src|action|formaction)\s*=\s*['"]?\s*javascript\s*:/i, message: 'javascript: URLs are not supported in live artifact previews' },
{ pattern: /\bdata-od-(?:html|raw|bind-html)\b/i, message: 'raw HTML insertion directives are not supported' },
];
export function validateHtmlTemplateV1Security(templateHtml: string): void {
for (const { pattern, message } of EXECUTABLE_TEMPLATE_PATTERNS) {
if (pattern.test(templateHtml)) throw new Error(message);
}
}
export function escapeHtmlTemplateValue(value: unknown): string {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
/**
* A binding resolver for one scope. Given a trimmed binding path (e.g.
* `data.title` or a loop variable path like `item.label`) it returns the
* already-escaped scalar string to substitute, or throws for an unsupported
* path. Loop scopes delegate non-matching heads (including `data.*`) to their
* parent so global bindings keep working inside a repeat.View on GitHub (pinned to 5be4028344)