OWASP Top 10 Security Priorities for Vue.js Developers

By , Senior Full-Stack Engineer12 min read

Security in Vue.js applications is crucial for protecting user data and maintaining application integrity. The OWASP Top 10 provides a framework for understanding the most critical web application vulnerabilities.

This guide translates these security priorities into practical Vue.js development practices, helping developers build secure applications from the ground up using Vue 3 Composition API.

Security in web applications requires systematic attention to common vulnerabilities and secure coding practices. This guide demonstrates practical approaches to identifying and mitigating security risks, using the OWASP Top 10 as foundational guidelines for building applications that are secure by design.

1. Broken Access Control

Access control vulnerabilities occur when users can act outside of their intended permissions. In Vue.js applications, this often manifests through inadequate route protection and missing authorization checks.

Vue.js Implementation Example

// composables/useAuth.js
// SPA-only pattern. For SSR (Nuxt, Vike, etc.) move state into a per-request store
// like Pinia — module-level refs leak across requests.
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'

const user = ref(null)
const isAuthenticated = ref(false)

export function useAuth() {
  const router = useRouter()

  const hasRole = (role) => user.value?.roles?.includes(role)

  const requireAuth = () => {
    if (!isAuthenticated.value) {
      router.push('/login')
      return false
    }
    return true
  }

  const requireRole = (role) => {
    if (!requireAuth() || !hasRole(role)) {
      router.push('/unauthorized')
      return false
    }
    return true
  }

  return { isAuthenticated, hasRole, requireAuth, requireRole }
}
// router/index.js - Route Guards
router.beforeEach((to, from, next) => {
  const { isAuthenticated, hasRole } = useAuth()

  if (to.meta.requiresAuth && !isAuthenticated.value) {
    next('/login')
  } else if (to.meta.requiresRole && !hasRole(to.meta.requiresRole)) {
    next('/unauthorized')
  } else {
    next()
  }
})

Security Best Practices

  • Always validate permissions on the backend - frontend checks are for UX only
  • Use route guards to protect sensitive pages
  • Implement role-based access control (RBAC) systematically
  • Never rely solely on hiding UI elements for security

📚 Learn More: Vue Router Navigation Guards | OWASP: Broken Access Control

2. Cryptographic Failures

Cryptographic failures expose sensitive data through weak encryption or improper implementation. In Vue.js applications, this commonly involves client-side data handling and secure communication.

Secure Data Handling in Vue.js

// composables/useSecureStorage.js
import { ref } from 'vue'

export function useSecureStorage() {
  // Never store sensitive data in localStorage/sessionStorage
  const setSecureData = (key, value) => {
    // Use httpOnly cookies for sensitive data via API calls
    return fetch('/api/secure-storage', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include', // Include httpOnly cookies
      body: JSON.stringify({ key, value }),
    })
  }

  const getSecureData = async (key) => {
    const response = await fetch(`/api/secure-storage/${key}`, {
      credentials: 'include',
    })
    return response.json()
  }

  return { setSecureData, getSecureData }
}
// HTTPS and Secure Headers Configuration
// Always use HTTPS in production
const apiClient = axios.create({
  baseURL:
    process.env.NODE_ENV === 'production'
      ? 'https://api.yourapp.com'
      : 'http://localhost:3000',
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
  },
})

Security Best Practices

  • Always use HTTPS in production environments
  • Never store sensitive data in localStorage or sessionStorage
  • Use httpOnly cookies for authentication tokens
  • Implement proper Content Security Policy (CSP) headers
  • Use Strict-Transport-Security (HSTS) and let the browser handle TLS validation

📚 Learn More: MDN: Transport Layer Security | OWASP: Cryptographic Failures

3. Injection Attacks

Injection flaws occur when untrusted data is sent to an interpreter. In Vue.js applications, this primarily involves Cross-Site Scripting (XSS) and unsafe dynamic content rendering.

XSS Prevention in Vue.js

<!-- SECURE: Vue automatically escapes interpolations -->
<template>
  <div>
    <!-- Safe - Vue escapes HTML automatically -->
    <p>{{ userInput }}</p>

    <!-- DANGEROUS - Raw HTML insertion -->
    <!-- <p v-html="userInput"></p> -->

    <!-- SECURE: Sanitized HTML if v-html is necessary -->
    <p v-html="sanitizedUserInput"></p>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import DOMPurify from 'dompurify'

const props = defineProps(['userInput'])

// Always sanitize before using v-html
const sanitizedUserInput = computed(() => {
  return DOMPurify.sanitize(props.userInput)
})
</script>
// composables/useSafeInput.js
import { ref, computed } from 'vue'
import DOMPurify from 'dompurify'

export function useSafeInput() {
  const rawInput = ref('')

  // Sanitize input for safe display
  const sanitizedInput = computed(() => {
    return DOMPurify.sanitize(rawInput.value, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
      ALLOWED_ATTR: [],
    })
  })

  // Encode for safe insertion into HTML output. Do NOT use this for JSON API
  // payloads — JSON encodes itself and HTML-escaping user data corrupts it.
  const escapeForHtmlOutput = (input) => {
    return input
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;')
  }

  return { rawInput, sanitizedInput, escapeForHtmlOutput }
}

Security Best Practices

  • Avoid v-html directive with user-generated content
  • Always sanitize dynamic content using libraries like DOMPurify
  • Use Content Security Policy (CSP) to prevent XSS attacks
  • Validate and escape user input on both client and server
  • Implement proper input validation with whitelisting

📚 Learn More: Vue.js Security Guide | OWASP: Injection

4. Insecure Design

Insecure design represents missing or ineffective control design. In Vue.js applications, this often involves missing security controls, inadequate authentication flows, and poor security architecture decisions.

Secure Vue.js Architecture Patterns

// composables/useSecureForm.js
import { ref } from 'vue'

export function useSecureForm() {
  const isSubmitting = ref(false)
  const errors = ref({})
  const csrfToken = ref('')

  // Get CSRF token from server. Pair this with `SameSite=Lax` (or `Strict`)
  // session cookies — the cookie attribute is the primary CSRF defense in
  // modern browsers; the token is the second layer.
  const initializeForm = async () => {
    try {
      const response = await fetch('/api/csrf-token', {
        credentials: 'include',
      })
      const data = await response.json()
      csrfToken.value = data.token
    } catch (error) {
      console.error('Failed to get CSRF token:', error)
    }
  }

  const submitForm = async (formData) => {
    if (isSubmitting.value) return

    isSubmitting.value = true
    errors.value = {}

    try {
      const response = await fetch('/api/form-submit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrfToken.value,
        },
        credentials: 'include',
        body: JSON.stringify(formData),
      })

      if (!response.ok) {
        const errorData = await response.json()
        errors.value = errorData.errors || {}
        throw new Error('Form submission failed')
      }

      return await response.json()
    } finally {
      isSubmitting.value = false
    }
  }

  return { isSubmitting, errors, submitForm, initializeForm }
}

Security Best Practices

  • Implement defense in depth with multiple security layers
  • Set SameSite=Lax (or Strict) on session cookies — this is the primary CSRF defense; tokens are the secondary layer
  • Design with principle of least privilege
  • Implement proper session management
  • Use secure design patterns like fail-safe defaults

📚 Learn More: OWASP Secure Coding Practices | OWASP: Insecure Design

5. Security Misconfiguration

Security misconfiguration occurs when security settings are not properly implemented. Vue.js applications are vulnerable through inadequate build configurations, exposed development features, and missing security headers.

Production-Ready Vue.js Configuration

// vite.config.js - Production Security Configuration
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  define: {
    // Strip Vue devtools from production builds
    __VUE_PROD_DEVTOOLS__: false,
    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
  },
  build: {
    // Source maps off in production so internal logic is not handed to attackers
    sourcemap: process.env.NODE_ENV !== 'production',
    // Vite's default esbuild minifier drops console/debugger via `esbuild.drop`
  },
  esbuild: {
    drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
  },
  server: {
    // Headers worth setting at the dev server. Skip the legacy `X-XSS-Protection`
    // header — modern browsers ignore it and OWASP recommends omitting it.
    // CSP is what blocks XSS in 2026.
    headers: {
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY',
      'Referrer-Policy': 'strict-origin-when-cross-origin',
    },
  },
})
// Environment Configuration
// Vite only exposes variables prefixed with `VITE_` to the client bundle.
// Anything else stays server-side. (Vue CLI used `VUE_APP_` — that's gone.)

// .env.production
VITE_API_URL=https://api.production.com
VITE_LOG_LEVEL=error

// .env.development
VITE_API_URL=http://localhost:3000
VITE_LOG_LEVEL=debug

// Never put secrets in any client-side env var. `VITE_*` values ship in the
// bundle in plain text — treat them as public. Secrets belong in server-only
// runtime config (env vars on the API host, never imported by the client).

Security Best Practices

  • Disable development features in production builds
  • Remove console logs and debug statements from production
  • Configure proper security headers
  • Use environment variables correctly (no secrets in frontend)
  • Implement proper error handling that doesn't expose system information

📚 Learn More: Vite Environment Variables | OWASP: Security Misconfiguration

6. Vulnerable and Outdated Components

Using components with known vulnerabilities puts your application at risk. Vue.js applications depend on numerous npm packages that require regular security updates and vulnerability monitoring.

Dependency Security Management

# Regular security auditing commands
npm audit
npm audit fix

# Check for outdated packages
npm outdated

# Use npm-check-updates for safe updates
npx npm-check-updates
npx npm-check-updates -u

# Audit with detailed information
npm audit --audit-level=moderate
// package.json - Security-focused configuration
{
  "scripts": {
    "audit": "npm audit",
    "audit:fix": "npm audit fix",
    "audit:check": "npm audit --audit-level=high",
    "update:check": "npx npm-check-updates",
    "prebuild": "npm run audit:check"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "@vue/runtime-core": "^3.4.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "vite": "^5.0.0"
  }
}

Security Best Practices

  • Run npm audit regularly and fix vulnerabilities promptly
  • Keep Vue.js and all dependencies updated to latest stable versions
  • Use tools like Snyk or GitHub Dependabot for automated monitoring
  • Review package dependencies before installation
  • Implement automated security scanning in CI/CD pipeline

📚 Learn More: npm Security Auditing | OWASP: Vulnerable Components

7. Identification and Authentication Failures

Authentication failures allow attackers to compromise passwords, keys, or session tokens. Vue.js applications must implement robust authentication mechanisms and secure session management.

Secure Authentication Implementation

// composables/useAuthentication.js
//
// Client-side throttling here is a UX nicety, NOT a security control. The
// server must enforce its own per-account and per-IP lockout — anyone hitting
// /api/auth/login with curl bypasses everything below. Treat the client
// counter as "stop pestering the user with a button that won't work."
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'

const user = ref(null)
const isLoading = ref(false)
const cooldownUntil = ref(null) // server-supplied retry hint, see below

export function useAuthentication() {
  const router = useRouter()

  const isAuthenticated = computed(() => !!user.value)
  const inCooldown = computed(() => {
    return cooldownUntil.value && Date.now() < cooldownUntil.value
  })

  const login = async (credentials) => {
    if (inCooldown.value) {
      throw new Error('Account temporarily locked. Please try again later.')
    }

    isLoading.value = true

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include', // session cookie should be HttpOnly + Secure + SameSite=Lax
        body: JSON.stringify({
          email: credentials.email,
          password: credentials.password,
          captchaToken: credentials.captchaToken,
        }),
      })

      if (response.status === 429) {
        // Honour the server's Retry-After header (seconds) for the UI cooldown
        const retryAfter = Number(response.headers.get('Retry-After')) || 900
        cooldownUntil.value = Date.now() + retryAfter * 1000
        throw new Error('Too many attempts. Try again later.')
      }

      if (!response.ok) {
        throw new Error('Invalid credentials')
      }

      const data = await response.json()
      user.value = data.user
      cooldownUntil.value = null

      router.push('/dashboard')
    } finally {
      isLoading.value = false
    }
  }

  const logout = async () => {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        credentials: 'include',
      })
    } finally {
      user.value = null
      router.push('/login')
    }
  }

  return { isAuthenticated, isLoading, inCooldown, login, logout }
}

Security Best Practices

  • Use secure session management with HttpOnly + Secure + SameSite cookies
  • Enforce account lockout on the server; client counters are UX only
  • Offer phishing-resistant MFA — passkeys (WebAuthn) where available, TOTP as the fallback
  • Never store passwords, refresh tokens, or session tokens in localStorage
  • Honour the server's Retry-After header in the UI on 429 responses
  • Implement proper session timeout and renewal

📚 Learn More: Auth0: Authentication Best Practices | OWASP: Authentication Failures

8. Software and Data Integrity Failures

Integrity failures occur when code and infrastructure don't protect against integrity violations. In Vue.js applications, this includes insecure CI/CD pipelines, compromised dependencies, and lack of integrity verification.

Build Pipeline Security

# .github/workflows/security-check.yml
name: Security Check
on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run security audit
        run: npm audit --audit-level=high

      - name: Run dependency check
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

      - name: Build application
        run: npm run build

      - name: Verify build integrity
        run: |
          # Check that build artifacts are generated correctly
          ls -la dist/
          # Verify no sensitive data in build
          ! grep -r "password\|secret\|key" dist/ || exit 1
// Build verification script
// scripts/verify-build.js
import { readFileSync, readdirSync } from 'fs'
import { join } from 'path'
import crypto from 'crypto'

const distDir = './dist'
const manifestFile = './build-manifest.json'

// Generate checksums for build artifacts
function generateManifest() {
  const manifest = {}
  const files = readdirSync(distDir, { recursive: true })

  for (const file of files) {
    if (file.endsWith('.js') || file.endsWith('.css')) {
      const filePath = join(distDir, file)
      const content = readFileSync(filePath)
      const hash = crypto.createHash('sha256').update(content).digest('hex')
      manifest[file] = hash
    }
  }

  return manifest
}

// Verify build integrity
const manifest = generateManifest()
console.log('Build manifest generated:', manifest)

Security Best Practices

  • Use package-lock.json and verify dependency integrity
  • Implement automated security scanning in CI/CD
  • Use Subresource Integrity (SRI) for external resources
  • Verify build artifacts and implement integrity checks
  • Use signed commits and protected branches
  • Monitor for supply chain attacks on dependencies

📚 Learn More: GitHub Actions Security | OWASP: Integrity Failures

9. Security Logging and Monitoring Failures

Insufficient logging and monitoring allows breaches to go undetected. Vue.js applications need comprehensive logging strategies and security monitoring to detect and respond to threats effectively.

Security Logging Implementation

// composables/useSecurityLogger.js
//
// Frontend telemetry only. The browser's view of the world is attacker-controlled
// (user-agent, timing, even the URL), so this is supplementary signal — the
// authoritative log lives on the server, where it can record the real IP,
// authenticated user ID from the session cookie, and request shape.
import { ref } from 'vue'

const securityEvents = ref([])

export function useSecurityLogger() {
  const logSecurityEvent = async (event) => {
    const securityEvent = {
      timestamp: new Date().toISOString(),
      type: event.type,
      severity: event.severity,
      // Don't pass userId from the client — server resolves it from the session
      details: event.details,
      url: window.location.href,
    }

    securityEvents.value.push(securityEvent)

    try {
      await fetch('/api/security/log', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(securityEvent),
      })
    } catch (error) {
      console.error('Failed to log security event:', error)
    }
  }

  const logLoginAttempt = (success) => {
    logSecurityEvent({
      type: 'authentication',
      severity: success ? 'info' : 'warning',
      details: {
        action: 'login_attempt',
        success,
        failureReason: success ? null : 'invalid_credentials',
      },
    })
  }

  const logSuspiciousActivity = (details) => {
    logSecurityEvent({
      type: 'security_violation',
      severity: 'high',
      details: {
        ...details,
        action: 'suspicious_activity_detected',
      },
    })
  }

  return { logSecurityEvent, logLoginAttempt, logSuspiciousActivity }
}

Security Best Practices

  • Treat browser-side logs as supplementary; the authoritative log is server-side
  • Log authentication events and access control failures on the server (with the real IP and authenticated user ID from the session)
  • Use structured logging (JSON) so events are queryable and alertable
  • Forward logs to a managed sink (Datadog, Sentry, CloudWatch, Loki) — local files are easy to lose
  • Set up alerts for repeated 401/403 patterns and unusual geo/UA changes
  • Document an incident response runbook so alerts have a defined path

📚 Learn More: OWASP ASVS Logging Requirements | OWASP: Logging Failures

10. Server-Side Request Forgery (SSRF)

SSRF flaws occur when web applications fetch remote resources without validating user-supplied URLs. SSRF is a backend vulnerability — the danger is the server being tricked into connecting to internal addresses. Frontend allowlisting catches honest mistakes and reduces noise, but it never replaces server-side validation: anyone with curl bypasses the browser entirely. Treat this section as defense-in-depth for the UI layer.

Safe URL Handling in Vue.js

// composables/useSafeUrlHandler.js
import { ref } from 'vue'

const allowedDomains = [
  'api.yourapp.com',
  'cdn.yourapp.com',
  'trusted-partner.com',
]

export function useSafeUrlHandler() {
  const isValidUrl = (url) => {
    try {
      const urlObj = new URL(url)

      // Only allow HTTPS for external URLs
      if (urlObj.protocol !== 'https:' && urlObj.protocol !== 'http:') {
        return false
      }

      // Check against whitelist of allowed domains
      return allowedDomains.includes(urlObj.hostname)
    } catch {
      return false
    }
  }

  const safeFetch = async (url, options = {}) => {
    if (!isValidUrl(url)) {
      throw new Error('Invalid or unauthorized URL')
    }

    // Add security headers
    const safeOptions = {
      ...options,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        ...options.headers,
      },
    }

    return fetch(url, safeOptions)
  }

  const validateImageUrl = (imageUrl) => {
    if (!isValidUrl(imageUrl)) {
      return '/images/placeholder.png' // Fallback image
    }
    return imageUrl
  }

  return { isValidUrl, safeFetch, validateImageUrl }
}
// Example: Safe image loading component
<template>
  <div>
    <img
      :src="safeImageUrl"
      :alt="imageAlt"
      @error="handleImageError"
      loading="lazy"
    />
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useSafeUrlHandler } from '@/composables/useSafeUrlHandler'

const props = defineProps(['imageUrl', 'imageAlt'])
const { validateImageUrl } = useSafeUrlHandler()

const safeImageUrl = computed(() => {
  return validateImageUrl(props.imageUrl)
})

const handleImageError = (event) => {
  // Fallback to placeholder if image fails to load
  event.target.src = '/images/placeholder.png'
}
</script>

Security Best Practices

  • The actual SSRF defense is on the server — validate, resolve DNS, and block private/link-local ranges before any outbound fetch
  • On the client, allowlist known domains so honest user input doesn't hit the wrong host
  • Never pass user-controlled URLs straight through to a backend that fetches them — the backend must re-validate
  • Use relative URLs for internal resources so allowlist drift can't accidentally widen
  • Pair backend validation with network segmentation and egress firewall rules

📚 Learn More: PortSwigger: SSRF | OWASP: SSRF

Building Secure Vue.js Applications

Implementing security in Vue.js applications requires a comprehensive approach that addresses each of the OWASP Top 10 vulnerabilities. By following the patterns and practices outlined in this guide, you can build robust, secure applications that protect user data and maintain system integrity.

Key security principles for Vue.js developers:

  • Security is a continuous process, not a one-time implementation
  • Always validate and sanitize user input on both client and server
  • Implement defense in depth with multiple security layers
  • Keep dependencies updated and monitor for vulnerabilities
  • Use HTTPS everywhere and implement proper authentication
  • Log security events and monitor for suspicious activities
  • Regular security testing and code reviews are essential

Remember that frontend security is just one part of a comprehensive security strategy. Always coordinate with backend developers and security teams to ensure end-to-end protection of your applications.

Contact

Drop me a line. I read everything and reply within a day.

Required fields are marked “(required)”.