XSS Prevention
Cross-Site Scripting (XSS) attacks are particularly dangerous in medical applications. An attacker injecting script into CPR could steal authentication tokens, access patient records, or manipulate billing data. Vue 3 provides strong built-in protections, but developers must understand the boundaries and avoid patterns that bypass them.
Vue's Built-in Escaping
Vue automatically escapes all content rendered through template interpolation (). This is the primary defense against XSS.
<template>
<!-- Safe -- Vue escapes the output -->
<p>{{ patient.last_name }}, {{ patient.first_name }}</p>
<p>{{ patient.address }}</p>
<p>{{ searchQuery }}</p>
</template>If patient.last_name contained <script>alert('xss')</script>, Vue would render it as literal text, not execute it:
<!-- What the browser actually renders -->
<p><script>alert('xss')</script>, Juan</p>This escaping applies to:
interpolation:attributebindings (attribute values are also escaped)v-textdirective
The v-html Danger
The v-html directive renders raw HTML without escaping. This is the most common source of XSS vulnerabilities in Vue applications.
<template>
<!-- DANGEROUS -- never use v-html with user-provided content -->
<div v-html="patient.notes"></div>
<!-- DANGEROUS -- data from API could contain injected HTML -->
<div v-html="visitRemarks"></div>
</template>When v-html Might Be Used
In rare cases, v-html may be needed for rendering formatted content (such as rich text notes from a WYSIWYG editor). In those cases, always sanitize the HTML before rendering:
import DOMPurify from 'dompurify';
const sanitizedNotes = computed(() =>
DOMPurify.sanitize(patient.value.notes, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: [],
})
);<template>
<!-- Sanitized -- only safe tags are allowed -->
<div v-html="sanitizedNotes"></div>
</template>CPR Policy on v-html
Do not use v-html in CPR unless there is a specific, documented need (such as rendering rich text from a trusted source). If you must use it:
- Sanitize with DOMPurify.
- Restrict allowed tags to the minimum needed.
- Remove all attributes (especially
onclick,onerror,hrefwithjavascript:). - Document why
v-htmlis necessary in a code comment.
Avoiding innerHTML
Never use innerHTML directly in JavaScript. Use Vue's template system instead:
// DANGEROUS -- bypasses Vue's escaping
document.getElementById('patient-name')!.innerHTML = patient.last_name;
// Safe -- let Vue handle the rendering
const patientName = ref(patient.last_name);
// Then use {{ patientName }} in the templateURL Sanitization
Be careful with dynamic URLs, especially in href and src attributes:
<template>
<!-- DANGEROUS -- if photoUrl contains "javascript:alert('xss')" -->
<a :href="patient.photoUrl">View Photo</a>
<!-- Safe -- validate the URL protocol -->
<a :href="sanitizedUrl">View Photo</a>
</template>
<script setup lang="ts">
const sanitizedUrl = computed(() => {
const url = patient.value.photo_url;
if (url && (url.startsWith('https://') || url.startsWith('http://'))) {
return url;
}
return '#';
});
</script>Content Security Policy (CSP)
CSP headers should be configured at the web server or CDN level to restrict what resources the browser can load. A recommended CSP for CPR:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com https://*.sentry.io;
font-src 'self';
object-src 'none';
frame-ancestors 'none';Key directives:
| Directive | Value | Purpose |
|---|---|---|
script-src 'self' | Only allow scripts from the same origin | Prevents inline script injection |
style-src 'self' 'unsafe-inline' | Allow inline styles (needed for Tailwind and Vue) | Vue and Tailwind use inline styles |
connect-src | API domain and Sentry | Restricts where fetch/XHR can connect |
object-src 'none' | No plugins | Prevents Flash/Java object embedding |
frame-ancestors 'none' | Prevents embedding in iframes | Clickjacking protection |
Note: 'unsafe-inline' for styles is required by Tailwind and Vue's scoped styles. This is a known trade-off.
Secure Data Handling Patterns
Never Interpolate into JavaScript Context
<script setup lang="ts">
// DANGEROUS -- if patientId comes from URL and contains script
const query = `SELECT * FROM patients WHERE id = ${patientId}`;
// Safe -- use parameterized API calls
const patient = await useApiFetch<Patient>(`/patients/${encodeURIComponent(patientId)}`);
</script>Sanitize User Input Before Display
While Vue escapes template output, validate and sanitize input data at the point of entry:
// Validate and clean search input
const sanitizeSearch = (input: string): string => {
return input.replace(/[<>"'&]/g, '').trim();
};Encode Dynamic Attributes
When building dynamic attributes, ensure values are properly encoded:
<template>
<!-- Safe -- Vue escapes attribute bindings -->
<img :alt="patient.first_name" :src="patient.photo_thumbnail" />
<!-- DANGEROUS -- string concatenation in template -->
<div :id="'patient-' + unsafeInput"></div>
</template>Sentry for Security Monitoring
CPR's Sentry integration helps detect potential XSS attempts by monitoring for:
- Unexpected JavaScript errors on pages handling user input
- Failed API requests that may indicate injection attempts
- Client-side errors in authentication flows
// From auth.sevice.ts -- errors are captured and reported
Sentry.captureException(err);Review Sentry alerts regularly for patterns that may indicate security probing.
Checklist
| Practice | Description |
|---|---|
Use for all user data | Vue's built-in escaping handles XSS |
Never use v-html with user input | Raw HTML rendering bypasses escaping |
Validate URLs before binding to href/src | Prevent javascript: protocol injection |
| Sanitize rich text with DOMPurify | If v-html is absolutely necessary |
| Configure CSP headers | Server-level defense-in-depth |
| Monitor Sentry for anomalies | Detect potential attack patterns |
| Strip console in production | Prevents information leakage |
