nexu-io/open-design · error · Error

iframe elements are not supported in live artifact previews

Error message

iframe elements are not supported in live artifact previews

What it means

Thrown by validateHtmlTemplateV1Security when the template matches /<\s*iframe\b/i. iframes are blocked because live artifact previews are rendered into a controlled host document; embedded frames could break out of the intended content boundary, load untrusted origins, or be used for clickjacking/SSRF via src URLs. The preview surface stays frame-free.

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('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}

/**
 * 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)

Solutions

  1. Replace the iframe with native HTML rendering of the data (e.g. show a thumbnail image linked to the external URL, do not embed it).
  2. If the goal is to display external content, surface it as data (a link, an image) rather than a nested browsing context.
  3. Request a first-party embed directive if you genuinely need framed content; do not bypass.

Example fix

// before
<template><iframe src='{{data.url}}'></iframe></template>
// after
<template><a href='{{data.url}}'>Open</a></template>
Defensive patterns

Strategy: validation

Validate before calling

function assertNoIframe(html: string): void {
  if (/<\s*iframe\b/i.test(html)) throw new Error('iframe tags not allowed');
}

Type guard

function isIframeFree(html: string): boolean {
  return !/<\s*iframe\b/i.test(html);
}

Try / catch

try { validateHtmlTemplateV1Security(tpl); } catch (e) { /* reject template */ throw e; }

Prevention

When it happens

Trigger: Template contains <iframe src='https://...'></iframe>, <iframe srcdoc=...>, <IFRAME>, or any opening iframe tag with leading whitespace.

Common situations: Model embeds a YouTube/CodePen iframe for rich preview; developer pastes an embed snippet; intent to show another live artifact inside this one; misunderstanding the sandbox model.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/fed2138e89697de6. Report an issue: GitHub.