{"record":{"id":"4b82d657f032aadf","repo":"nexu-io/open-design","slug":"raw-html-insertion-directives-are-not-supported","errorCode":null,"errorMessage":"raw HTML insertion directives are not supported","messagePattern":"raw HTML insertion directives are not supported","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 contains a data-od-html, data-od-raw, or data-od-bind-html attribute (regex /\\bdata-od-(?:html|raw|bind-html)\\b/i). These would be raw-HTML insertion directives that bypass HTML escaping; the render pipeline supports only escaped interpolation ({{...}}) and explicit safe directives, so any raw-insertion directive is rejected up front to keep the model from injecting unescaped markup.","triggerScenarios":"<div data-od-html=\"{{data.body}}\">, <span data-od-raw=\"{{data.html}}\">, <p data-od-bind-html=\"...\">; appears in any case or with extra whitespace before the =.","commonSituations":"Model assumes a Vue/Angular-style v-html / ng-bind-html directive exists; developer expects to render markdown HTML output; misunderstanding that all {{...}} interpolation is already escaped and there is no opt-out.","solutions":["Use plain {{data.field}} interpolation; values are HTML-escaped automatically.","If the data genuinely contains HTML that must be rendered as markup, that is not supported by html_template_v1 — pre-render or strip tags before feeding the data."],"exampleFix":"// before\n<template><div data-od-html=\"{{data.body}}\"></div></template>\n// after\n<template><div>{{data.body}}</div></template>","handlingStrategy":"validation","validationCode":"function assertNoRawHtmlDirective(html: string): void {\n  if (/\\bdata-od-(?:html|raw|bind-html)\\b/i.test(html)) {\n    throw new Error('raw-HTML directives are not supported');\n  }\n}","typeGuard":"function isRawDirectiveFree(html: string): boolean {\n  return !/\\bdata-od-(?:html|raw|bind-html)\\b/i.test(html);\n}","tryCatchPattern":"try { validateHtmlTemplateV1Security(tpl); } catch (e) { throw e; }","preventionTips":["Remember there is no opt-out from HTML escaping in html_template_v1.","If you need to render HTML, pre-process the data before storing in data.json.","Document the directive vocabulary so authors do not invent v-html-style directives."],"tags":["security","xss","html-directive","live-artifacts","validation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}