Prevención de XSS en Vue
Cross-Site Scripting (XSS) es un ataque en el que un atacante inyecta código JavaScript malicioso en una página web para robar datos, secuestrar sesiones o redirigir a los usuarios.
Auto-escaping en templates
Vue proporciona protección automática contra XSS en las interpolaciones de texto con doble llave {{ }}. Todo el contenido se escapa automáticamente como texto plano:
<script setup lang="ts">
import { ref } from 'vue'
// Aunque el valor contenga HTML malicioso, Vue lo escapa automáticamente
const userInput = ref('<script>alert("XSS")</script>')
const safeMessage = ref('<b>Texto en negrita</b>')
</script>
<template>
<div>
<!-- Se muestra como texto plano, NO se ejecuta -->
<p>{{ userInput }}</p>
<!-- Resultado visible: <script>alert("XSS")</script> -->
<!-- También escapado -->
<p>{{ safeMessage }}</p>
<!-- Resultado visible: <b>Texto en negrita</b> -->
</div>
</template>
El binding de atributos con v-bind (:attr) también escapa automáticamente los valores, previniendo la inyección en atributos HTML.
Peligros de v-html
La directiva v-html renderiza HTML sin escapar, lo que la convierte en un vector de ataque XSS si se usa con contenido no confiable:
<script setup lang="ts">
import { ref } from 'vue'
// PELIGROSO: contenido de usuario sin sanitizar
const untrustedContent = ref('<img src="x" onerror="alert(document.cookie)">')
// SEGURO: contenido controlado por el desarrollador
const trustedContent = ref('<strong>Texto seguro</strong>')
</script>
<template>
<div>
<!-- NUNCA usar v-html con contenido de usuario sin sanitizar -->
<div v-html="untrustedContent"></div>
<!-- Solo usar v-html con contenido que controlamos -->
<div v-html="trustedContent"></div>
</div>
</template>
Sanitización con DOMPurify
Cuando es necesario renderizar HTML de fuentes externas, se debe sanitizar con una librería como DOMPurify:
npm install dompurify
npm install -D @types/dompurify
// src/utils/sanitize.ts
import DOMPurify from 'dompurify'
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target', 'rel']
})
}
<script setup lang="ts">
import { ref, computed } from 'vue'
import { sanitizeHtml } from '@/utils/sanitize'
const rawContent = ref('<p>Texto <b>válido</b></p><script>alert("xss")</script>')
const cleanContent = computed(() => sanitizeHtml(rawContent.value))
</script>
<template>
<!-- El script malicioso se elimina, solo queda el HTML permitido -->
<div v-html="cleanContent"></div>
</template>
La configuración de DOMPurify permite especificar exactamente qué etiquetas y atributos son válidos, eliminando todo lo demás.
Protección contra CSRF
Cross-Site Request Forgery (CSRF) es un ataque que engaña al navegador del usuario para que realice peticiones no deseadas a un servidor donde está autenticado.
Implementar tokens CSRF con Axios
La protección CSRF funciona con un token que el servidor genera y el cliente debe incluir en cada petición que modifique datos:
// src/services/api.ts
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
// Enviar cookies automáticamente (necesario para CSRF)
withCredentials: true
})
// Configurar Axios para leer el token CSRF de las cookies
// y enviarlo en la cabecera de cada petición
api.defaults.xsrfCookieName = 'XSRF-TOKEN' // Cookie donde el servidor pone el token
api.defaults.xsrfHeaderName = 'X-XSRF-TOKEN' // Cabecera donde Axios envía el token
export default api
Con esta configuración, Axios lee automáticamente el token CSRF de la cookie XSRF-TOKEN y lo incluye en la cabecera X-XSRF-TOKEN de cada petición POST, PUT, PATCH y DELETE.
Configuración de cookies seguras
Cuando se trabaja con cookies para autenticación, es importante configurarlas correctamente:
// En el servidor (ejemplo conceptual de la respuesta)
// Set-Cookie: XSRF-TOKEN=abc123; Path=/; SameSite=Strict; Secure
// Set-Cookie: refreshToken=xyz789; Path=/; HttpOnly; SameSite=Strict; Secure
Las propiedades clave de las cookies seguras son:
- HttpOnly: la cookie no es accesible desde JavaScript, protegiéndola de XSS.
- Secure: la cookie solo se envía por HTTPS.
- SameSite=Strict: la cookie no se envía en peticiones cross-origin, previniendo CSRF.
Content Security Policy (CSP)
La Content Security Policy es una cabecera HTTP que indica al navegador qué recursos puede cargar la página. Es una defensa en profundidad contra XSS:
<!-- En el index.html o configurado en el servidor -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.midominio.com;
">
En un proyecto Vite, se puede configurar CSP en el servidor de producción o añadir la meta etiqueta en index.html. Las directivas principales son:
default-src 'self': por defecto, solo carga recursos del mismo origen.script-src 'self': solo permite scripts del mismo origen.connect-src: define los dominios a los que se pueden hacer peticiones AJAX/fetch.
Validación y sanitización de entrada
Aunque la validación definitiva debe realizarse siempre en el servidor, validar en el frontend mejora la experiencia de usuario y reduce peticiones innecesarias:
// src/utils/validation.ts
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
export function validatePassword(password: string): string[] {
const errors: string[] = []
if (password.length < 8) {
errors.push('La contraseña debe tener al menos 8 caracteres')
}
if (!/[A-Z]/.test(password)) {
errors.push('Debe contener al menos una mayúscula')
}
if (!/[a-z]/.test(password)) {
errors.push('Debe contener al menos una minúscula')
}
if (!/[0-9]/.test(password)) {
errors.push('Debe contener al menos un número')
}
return errors
}
export function sanitizeInput(input: string): string {
return input
.trim()
.replace(/[<>]/g, '') // Eliminar caracteres HTML básicos
}
<script setup lang="ts">
import { ref, computed } from 'vue'
import { validateEmail, validatePassword, sanitizeInput } from '@/utils/validation'
const email = ref('')
const password = ref('')
const emailError = computed(() => {
if (!email.value) return ''
return validateEmail(email.value) ? '' : 'Email no válido'
})
const passwordErrors = computed(() => validatePassword(password.value))
const isFormValid = computed(() =>
validateEmail(email.value) && passwordErrors.value.length === 0
)
function handleSubmit() {
const cleanEmail = sanitizeInput(email.value)
const cleanPassword = password.value // No sanitizar contraseñas
// Enviar credenciales al backend
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="email">Email</label>
<input id="email" v-model="email" type="email" />
<span v-if="emailError" class="error">{{ emailError }}</span>
</div>
<div>
<label for="password">Contraseña</label>
<input id="password" v-model="password" type="password" />
<ul v-if="passwordErrors.length > 0">
<li v-for="error in passwordErrors" :key="error" class="error">
{{ error }}
</li>
</ul>
</div>
<button type="submit" :disabled="!isFormValid">Enviar</button>
</form>
</template>
Cabeceras de seguridad
Las cabeceras HTTP de seguridad se configuran en el servidor, pero es importante conocerlas para solicitarlas al equipo de backend o configurarlas en el servidor de despliegue:
| Cabecera | Valor recomendado | Propósito |
|---|---|---|
| X-Content-Type-Options | nosniff | Evita que el navegador interprete archivos con un tipo MIME diferente al declarado |
| X-Frame-Options | DENY o SAMEORIGIN | Previene ataques de clickjacking impidiendo que la página se cargue en un iframe |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Fuerza el uso de HTTPS durante un periodo definido |
| Referrer-Policy | strict-origin-when-cross-origin | Controla qué información del referrer se envía con las peticiones |
| Permissions-Policy | camera=(), microphone=() | Restringe el acceso a APIs del navegador como cámara o micrófono |
En Vite, se pueden añadir cabeceras para el servidor de desarrollo:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
headers: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
}
}
})
Auditoría de dependencias
Las dependencias de terceros pueden contener vulnerabilidades conocidas. Es esencial mantenerlas actualizadas:
# Analizar vulnerabilidades en las dependencias
npm audit
# Corregir vulnerabilidades automáticamente cuando sea posible
npm audit fix
# Ver dependencias desactualizadas
npm outdated
# Actualizar dependencias a las últimas versiones compatibles
npm update
Se recomienda ejecutar npm audit de forma regular y revisar las dependencias antes de cada despliegue a producción.
Variables de entorno seguras
Vite expone al código del cliente solo las variables de entorno que comienzan con el prefijo VITE_. Esto es una medida de seguridad para evitar filtrar información sensible:
# .env
# PÚBLICO: accesible en el código del cliente
VITE_API_URL=https://api.midominio.com
VITE_APP_TITLE=Mi Aplicación
# PRIVADO: NO accesible en el código del cliente (solo en el servidor)
DATABASE_URL=postgresql://user:pass@localhost/db
JWT_SECRET=mi-clave-secreta-jwt
API_KEY_PRIVADA=sk-123456789
// Accesible (tiene prefijo VITE_)
const apiUrl = import.meta.env.VITE_API_URL
// NO accesible (no tiene prefijo VITE_)
// import.meta.env.DATABASE_URL --> undefined
// import.meta.env.JWT_SECRET --> undefined
Las claves secretas, contraseñas y credenciales de base de datos nunca deben incluir el prefijo VITE_. Además, el archivo .env con valores de producción no debe subirse al repositorio; se debe añadir a .gitignore y crear un .env.example con las variables sin valores reales.
Forzar HTTPS
Para asegurar que la aplicación siempre se sirva por HTTPS, se puede implementar una redirección en el frontend como medida adicional:
// src/utils/security.ts
export function enforceHttps(): void {
if (
location.protocol !== 'https:' &&
location.hostname !== 'localhost' &&
location.hostname !== '127.0.0.1'
) {
location.replace(`https://${location.host}${location.pathname}${location.search}`)
}
}
// main.ts
import { enforceHttps } from '@/utils/security'
if (import.meta.env.PROD) {
enforceHttps()
}
Esta verificación solo se activa en producción y excluye los entornos de desarrollo local. La redirección principal a HTTPS debe configurarse en el servidor web (Nginx, Apache, CDN), pero esta verificación en el frontend actúa como capa adicional de seguridad.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje