Skip to content

Last updated:

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.

vue
<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:

html
<!-- What the browser actually renders -->
<p>&lt;script&gt;alert('xss')&lt;/script&gt;, Juan</p>

This escaping applies to:

  • interpolation
  • :attribute bindings (attribute values are also escaped)
  • v-text directive

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.

vue
<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:

ts
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: [],
  })
);
vue
<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:

  1. Sanitize with DOMPurify.
  2. Restrict allowed tags to the minimum needed.
  3. Remove all attributes (especially onclick, onerror, href with javascript:).
  4. Document why v-html is necessary in a code comment.

Avoiding innerHTML

Never use innerHTML directly in JavaScript. Use Vue's template system instead:

ts
// 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 template

URL Sanitization

Be careful with dynamic URLs, especially in href and src attributes:

vue
<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:

DirectiveValuePurpose
script-src 'self'Only allow scripts from the same originPrevents inline script injection
style-src 'self' 'unsafe-inline'Allow inline styles (needed for Tailwind and Vue)Vue and Tailwind use inline styles
connect-srcAPI domain and SentryRestricts where fetch/XHR can connect
object-src 'none'No pluginsPrevents Flash/Java object embedding
frame-ancestors 'none'Prevents embedding in iframesClickjacking 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

vue
<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:

ts
// 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:

vue
<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
ts
// From auth.sevice.ts -- errors are captured and reported
Sentry.captureException(err);

Review Sentry alerts regularly for patterns that may indicate security probing.

Checklist

PracticeDescription
Use for all user dataVue's built-in escaping handles XSS
Never use v-html with user inputRaw HTML rendering bypasses escaping
Validate URLs before binding to href/srcPrevent javascript: protocol injection
Sanitize rich text with DOMPurifyIf v-html is absolutely necessary
Configure CSP headersServer-level defense-in-depth
Monitor Sentry for anomaliesDetect potential attack patterns
Strip console in productionPrevents information leakage

CPR - Clinical Patient Records