A Vue 3 page must render rich text from a CMS that users can edit; which rendering approaches are unsafe, and what would you ship instead?
answer
- escaping only covers text
- the raw-HTML directive is a sink
- never compile content as a template
- never mount on server-rendered user content
basics
~20 sIn Vue 3, v-html on unsanitized CMS HTML, compiling CMS strings as Vue templates, and mounting Vue over server-rendered user content are all unsafe. Sanitize with an allowlist before v-html, or render structured content through your own components.
solid answer
~50 sVue escapes `{{ }}` and attribute bindings, but it cannot protect three patterns. **`v-html` on raw CMS HTML** is an XSS sink; Vue does not sanitize it. **Compiling CMS strings as templates**, for example as a component `template` so editors can use `<RouterLink>`, is worse: template expressions run as JavaScript, so it is arbitrary code execution, and the docs call this rule number one. **Mounting Vue on a server-rendered page that contains user content** turns harmless-looking mustaches in that content into live template expressions. Ship instead: sanitize with an allowlist, ideally on the server when content is saved, then render with `v-html`; or have the CMS return **structured** content (blocks, marks, links) and render it with your own components, which also gives you router links and scoped styles. Sanitize user URLs on the backend too.
code
vue · 12 lines<script setup lang="ts">
import { computed } from 'vue'
import { sanitizeHtml } from './sanitize' // project wrapper around an allowlist sanitizer
const props = defineProps<{ html: string }>()
// sanitize in one place; the template never sees raw CMS HTML
const safeHtml = computed(() => sanitizeHtml(props.html))
</script>
<template>
<article class="cms-body" v-html="safeHtml"></article>
</template>go deeper
Know that v-html renders raw HTML and must never receive user-provided content that has not been sanitized.
Explain what Vue escapes automatically, text and attributes, and why v-html and compiled templates fall outside that protection.
Identify template injection alongside v-html XSS, choose server-side sanitizing or structured rendering, and plan for v-html's link and styling side effects.
Decide the content contract with the CMS team, structured blocks or sanitized HTML, so every client receives safe data by design.
## What Vue protects, and where it stops Vue's security guide lists what it does for you: **text interpolation** is escaped using native APIs such as `textContent`, and **attribute bindings** are escaped using APIs such as `setAttribute`. Neither can be broken out of with a crafted string. Everything past that is the application's responsibility. The guide names the patterns explicitly, and a CMS integration tends to hit all of them. ## Three unsafe approaches | Approach | Why teams try it | Why it is unsafe | |---|---|---| | `v-html="page.body"` on raw CMS HTML | fastest way to show formatting | `innerHTML` with attacker markup is an XSS sink; Vue does not sanitize | | Compiling `page.body` as a component `template` | editors want `<RouterLink>` or custom widgets | template expressions execute as JavaScript: arbitrary code, and during SSR it runs on the server | | Mounting the app over server-rendered HTML containing CMS text | progressive enhancement of an existing page | Vue compiles the in-DOM markup as a template, so `{{ }}` typed into content becomes a live expression | The second and third are **template injection**, and they are specific to template-compiling frameworks. The docs are blunt: never use non-trusted content as your component template, and never mount Vue on nodes that may contain server-rendered, user-provided content. The **restricted globals allowlist** for template expressions is not a security sandbox; the guide says it is impractical for Vue to fully shield you from malicious code in a template. ## What to ship instead 1. **Sanitize, then `v-html`.** Pass the HTML through an allowlist sanitizer before it reaches the component, preferably on the server when content is **saved**, so every client (web, mobile, email) gets clean data. Then `v-html` renders trusted markup. Keep the sanitizing step in one place, for example a computed that wraps the call, so no component binds raw HTML by accident. 2. **Render structure, not strings.** Ask the CMS for structured content, such as a JSON tree of paragraphs, headings, links and embeds, and map each node type to your own component. Links become real router links, embeds become real components, and scoped styles apply because everything goes through Vue's compiler. 3. **Handle URLs separately.** A link's `href` from content can carry a `javascript:` URL even when the HTML is clean; the guide recommends sanitizing user URLs on the backend before storage. 4. **Keep content out of mount targets.** If a server template must include CMS output, put it outside the element Vue mounts on, or deliver it through the API instead. ## Side effects of v-html to plan for Even sanitized HTML behaves differently from template output: - **Links do full page loads**, because they are plain anchors. Intercept clicks on the container with one handler and route internal links yourself, or use the structured approach. - **Scoped styles do not apply**; style the content with a global or module stylesheet. - **No components or directives** run inside the injected markup. ## How to answer in an interview Name the sink (`v-html`), the two template-injection traps (compiling content, mounting over content), and the two safe designs (sanitize-before-`v-html`, structured content through components). Then state the principle the guide ends on: if unsanitized user content is executed as HTML, JavaScript or CSS, the application is open to attack, whatever the framework.
- Why is compiling CMS content as a Vue 3 template worse than rendering it with `v-html`?`v-html` only parses HTML, so the risk is markup-level XSS that a sanitizer can address. A compiled template turns content into JavaScript expressions the app executes, and with SSR they run on the server. No sanitizer designed for HTML makes that safe, which is why the docs forbid untrusted templates outright.
- How do internal links inside sanitized `v-html` content avoid full page reloads in a Vue 3 app?They are plain anchors, so Vue's router components are not involved. Add one click listener on the container, check that the target is an internal link, prevent the default and navigate programmatically. Structured content rendered through components avoids the problem entirely.
saying these in an interview costs you the question
- Vue sanitizes HTML passed to v-html automatically.
- Compiling CMS content as a template is safe because of the globals allowlist.
- Escaping mustaches also protects content inside v-html.
- Mounting Vue on a server-rendered page is harmless if the content is plain HTML.
- Sanitizing once in the browser makes URLs safe for every client.