Vue.js
XSS Sinks in Vue.js
v-html Directive
The v-html directive renders raw HTML. <script> elements inserted through the underlying innerHTML operation are normally inert, but active attributes and elements such as an image onerror can still produce XSS. Never pass unsanitized user input to v-html.[1][3]
<div id="app">
<div v-html="htmlContent"></div>
</div>
<script>
new Vue({
el: '#app',
data: {
htmlContent: '<img src=x onerror=alert(1)>'
}
})
</script>
v-bind with src or href
Binding a user string to URL-bearing attributes (href, src, xlink:href, formaction …) lets payloads such as javascript:alert(1) run when the link is followed.[2][3]
<div id="app">
<a v-bind:href="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/userInput">Click me</a>
</div>
<script>
new Vue({
el: '#app',
data: {
userInput: 'javascript:alert(1)'
}
})
</script>
v-on with user-controlled handlers
If an attacker controls a Vue template, directive expressions such as v-on:click="..." become code in the compiled render function. Merely storing the string alert(1) in a data property does not execute it as JavaScript; the dangerous condition is untrusted template source.[3]
<div id="app">
<!-- Dangerous if an attacker controls this template expression: -->
<button v-on:click="alert(document.domain)">Click me</button>
</div>
<script>
new Vue({
el: '#app',
data: {}
})
</script>
Dynamic attribute / event names
User-supplied names in v-bind:[attr] or v-on:[event] expand the attack surface and can bypass application allowlists. Vue applies its own attribute/event handling and CSP still applies, so validate behavior against the deployed Vue version rather than assuming every on* name becomes a native handler.[3]
<img v-bind:[userAttr]="payload">
<!-- userAttr = 'onerror', payload = 'alert(1)' -->
SSR attribute-name splitting in object v-bind
When auditing SSR, test the keys of objects spread with v-bind="attrs", not only their values. @vue/server-renderer through 3.5.41 checked dynamic attribute names against a blacklist that omitted carriage return (U+000D). A raw CR in an attacker-controlled key therefore reached the HTML response; the browser normalised it to LF and split one intended name into several attributes. This can create a zero-interaction XSS when an object from a CMS, form builder, API or stored JSON is spread onto an element. The published advisory listed no patched version at publication time.[9]
// Reaches <input v-bind="attrs"> during SSR
const attrs = {
['x\rautofocus\ronfocus']: 'alert(document.domain)'
}
// Browser parses: x="" autofocus="" onfocus="alert(document.domain)"
This is an SSR-only parser differential: client-side Vue uses setAttribute(), which rejects such a name. Send the key through the real input path (for example, JSON "x\rautofocus\ronfocus"), inspect the raw response bytes before hydration, and verify whether intermediaries preserve the CR. A source-code review should trace both property names and values into v-bind="object" or equivalent render-function props.[9]
Dynamic component (<component :is>)
Allowing an untrusted component identifier in :is may expose components that were not intended for the user. A string containing <script> is not automatically compiled as an inline template; server compromise requires a separate path that treats attacker input as template source.[3]
<component :is="userChoice"></component>
<!-- userChoice must be restricted to an allowlist of intended components -->
Untrusted templates in SSR
During server-side rendering, the template runs on your server; injecting user HTML can escalate XSS to full Remote Code Execution (RCE). CVEs in vue-template-compiler prove the risk.[3]
// DANGER – never do this
const app = createSSRApp({ template: userProvidedHtml })
Vue CSTI payloads and version-specific compiler gadgets are documented in Client Side Template Injection; keep this page focused on Vue-specific sinks and SSR boundaries.
Filters / render functions that eval
Legacy filters that build render strings or call eval/new Function on user data are another XSS vector—replace them with computed properties.[3]
Vue.filter('run', code => eval(code)) // DANGER
Other Common Vulnerabilities in Vue Projects
Prototype pollution and HTML-context confusion in vue-i18n
vue-i18n’s flat-JSON resolver has accepted __proto__ paths that write to Object.prototype. Trace whether an attacker can supply locale objects or remote translation bundles, then look for polluted properties reaching a secondary gadget rather than treating the write alone as code execution.[4]
handleFlatJson({ '__proto__.polluted': 'yes' })
console.log(({}).polluted) // "yes" on affected versions
Do not treat escapeParameterHtml: true as a sanitizer for the final translation string. Affected vue-i18n releases escaped interpolated parameters as text but could still produce executable markup when a translation placed the parameter in an HTML attribute and the result was passed to v-html. This is particularly relevant for remotely managed translations and low-privileged locale editors.[10]
<!-- message: <img src=x onerror="{payload}"> -->
<p v-html="$t('warning', { payload: 'alert(document.domain)' })"></p>
Audit every $t()/t() flow into v-html and sanitize the complete rendered HTML under an explicit policy. The reviewed advisory identifies 9.14.5, 10.0.8 and 11.1.10 as the first patched release in each maintained branch.[10]
Open redirects with vue-router
Applications sometimes use an unchecked next value with window.location or an external-navigation helper, creating an open redirect. The earlier page specifically named router.push, <router-link>, and a javascript: destination. Preserve those as test inputs for application wrappers and dynamic URL props, but Vue Router navigation is normally route-oriented, so do not assume the bare router accepts every external scheme without validating the deployed version and normalization path.[3][6]
window.location.assign(this.$route.query.next) // DANGER: validate origin/scheme
Prefer a server-side mapping from a short identifier to an approved destination; if a URL must be accepted, apply an allowlist rather than a denylist.[6]
CSRF in Axios / fetch
SPAs still need deliberate CSRF defenses. SameSite=Lax or Strict blocks cookies on many cross-site requests, but coverage depends on method, navigation context, browser behavior, and whether sibling subdomains are attacker-controlled.[7]
axios.post('/api/transfer', data, {
headers: { 'X-CSRF-TOKEN': token }
})
Click-jacking
Prevent framing with CSP frame-ancestors; X-Frame-Options is a useful legacy fallback. Sending both is defense in depth, not a requirement for standards-compliant modern browsers.[8]
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy pitfalls
Runtime template compilation can conflict with a strict CSP. Prefer a runtime-only build with precompiled templates so the application does not need eval-like compilation; confirm the exact Vue version and build rather than adding 'unsafe-eval' blindly.[3]
Content-Security-Policy: default-src 'self'; script-src 'self';
Supply-chain attacks (node-ipc – March 2022)
The sabotage of node-ipc—pulled by Vue CLI—showed how a transitive dependency can run arbitrary code on dev machines. Pin versions and audit often.[5]
npm ci --ignore-scripts # safer install
Hardening Checklist
- Sanitise every string before it hits
v-html(DOMPurify). - Whitelist allowed schemes, attributes, components, and events; never spread attacker-controlled property names into SSR elements.
- Avoid
evaland dynamic templates altogether. - Patch dependencies weekly and monitor advisories.
- Send strong HTTP headers (CSP, HSTS, XFO, CSRF).
- Lock your supply chain with audits, lockfiles, and signed commits.
References
- [1] Vue XSS Guide: Examples and Prevention
- [2] Vue JS Security
- [3] Security | Vue.js
- [4] Vue I18n Allows Prototype Pollution in handleFlatJson (GHSA-p2ph-7g93-hw3m)
- [5] Alert: peacenotwar module sabotages npm developers in the node-ipc package to protest the invasion of Ukraine
- [6] OWASP - Unvalidated Redirects and Forwards Cheat Sheet
- [7] MDN - Secure cookie configuration and
SameSite - [8] MDN - Clickjacking defenses
- [9] XSS in
@vue/server-renderervia missing CR in attribute-name blacklist - [10]
vue-i18n’sescapeParameterHtmldoes not prevent DOM-based XSS through tag attributes