Skip to content

Security Best Practices for Element Plus Applications

Overview

This guide covers essential security practices for Element Plus applications, including input validation, authentication, authorization, data protection, and secure coding practices.

Input Validation and Sanitization

Form Input Validation

vue
<template>
  <el-form
    ref="formRef"
    :model="form"
    :rules="secureRules"
    label-width="120px"
    @submit.prevent="handleSecureSubmit"
  >
    <!-- Secure text input -->
    <el-form-item label="Username" prop="username">
      <el-input
        v-model="form.username"
        :maxlength="50"
        show-word-limit
        clearable
        @input="sanitizeInput('username', $event)"
      >
        <template #prefix>
          <el-icon><User /></el-icon>
        </template>
      </el-input>
    </el-form-item>
    
    <!-- Secure email input -->
    <el-form-item label="Email" prop="email">
      <el-input
        v-model="form.email"
        type="email"
        :maxlength="100"
        clearable
        @blur="validateEmail"
      >
        <template #prefix>
          <el-icon><Message /></el-icon>
        </template>
      </el-input>
    </el-form-item>
    
    <!-- Secure password input -->
    <el-form-item label="Password" prop="password">
      <el-input
        v-model="form.password"
        type="password"
        :maxlength="128"
        show-password
        autocomplete="new-password"
      >
        <template #prefix>
          <el-icon><Lock /></el-icon>
        </template>
      </el-input>
      <div class="password-strength">
        <el-progress
          :percentage="passwordStrength.percentage"
          :color="passwordStrength.color"
          :show-text="false"
        />
        <span class="strength-text">{{ passwordStrength.text }}</span>
      </div>
    </el-form-item>
    
    <!-- Secure file upload -->
    <el-form-item label="Avatar" prop="avatar">
      <el-upload
        ref="uploadRef"
        :action="uploadUrl"
        :headers="uploadHeaders"
        :before-upload="secureFileValidation"
        :on-success="handleUploadSuccess"
        :on-error="handleUploadError"
        :file-list="fileList"
        list-type="picture-card"
        :limit="1"
        accept="image/jpeg,image/png,image/webp"
      >
        <el-icon><Plus /></el-icon>
      </el-upload>
    </el-form-item>
    
    <!-- CAPTCHA verification -->
    <el-form-item label="Verification" prop="captcha">
      <el-row :gutter="10">
        <el-col :span="12">
          <el-input
            v-model="form.captcha"
            placeholder="Enter CAPTCHA"
            :maxlength="6"
          />
        </el-col>
        <el-col :span="12">
          <div class="captcha-container" @click="refreshCaptcha">
            <img :src="captchaUrl" alt="CAPTCHA" />
            <el-button size="small" text>Refresh</el-button>
          </div>
        </el-col>
      </el-row>
    </el-form-item>
    
    <el-form-item>
      <el-button
        type="primary"
        :loading="submitting"
        @click="handleSecureSubmit"
      >
        Submit
      </el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import DOMPurify from 'dompurify'
import { z } from 'zod'

const formRef = ref()
const uploadRef = ref()
const submitting = ref(false)
const captchaUrl = ref('')
const fileList = ref([])

const form = reactive({
  username: '',
  email: '',
  password: '',
  avatar: '',
  captcha: ''
})

// Secure validation schema using Zod
const validationSchema = z.object({
  username: z.string()
    .min(3, 'Username must be at least 3 characters')
    .max(50, 'Username must not exceed 50 characters')
    .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, hyphens, and underscores'),
  
  email: z.string()
    .email('Invalid email format')
    .max(100, 'Email must not exceed 100 characters'),
  
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .max(128, 'Password must not exceed 128 characters')
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, 
           'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'),
  
  captcha: z.string()
    .length(6, 'CAPTCHA must be exactly 6 characters')
})

// Element Plus validation rules
const secureRules = {
  username: [
    { required: true, message: 'Username is required', trigger: 'blur' },
    { 
      validator: (rule, value, callback) => {
        try {
          validationSchema.shape.username.parse(value)
          callback()
        } catch (error) {
          callback(new Error(error.errors[0].message))
        }
      }, 
      trigger: 'blur' 
    }
  ],
  
  email: [
    { required: true, message: 'Email is required', trigger: 'blur' },
    { 
      validator: (rule, value, callback) => {
        try {
          validationSchema.shape.email.parse(value)
          callback()
        } catch (error) {
          callback(new Error(error.errors[0].message))
        }
      }, 
      trigger: 'blur' 
    }
  ],
  
  password: [
    { required: true, message: 'Password is required', trigger: 'blur' },
    { 
      validator: (rule, value, callback) => {
        try {
          validationSchema.shape.password.parse(value)
          callback()
        } catch (error) {
          callback(new Error(error.errors[0].message))
        }
      }, 
      trigger: 'blur' 
    }
  ],
  
  captcha: [
    { required: true, message: 'CAPTCHA is required', trigger: 'blur' },
    { 
      validator: (rule, value, callback) => {
        try {
          validationSchema.shape.captcha.parse(value)
          callback()
        } catch (error) {
          callback(new Error(error.errors[0].message))
        }
      }, 
      trigger: 'blur' 
    }
  ]
}

// Password strength calculation
const passwordStrength = computed(() => {
  const password = form.password
  if (!password) return { percentage: 0, color: '#909399', text: 'No password' }
  
  let score = 0
  
  // Length check
  if (password.length >= 8) score += 20
  if (password.length >= 12) score += 10
  
  // Character variety checks
  if (/[a-z]/.test(password)) score += 20
  if (/[A-Z]/.test(password)) score += 20
  if (/\d/.test(password)) score += 20
  if (/[@$!%*?&]/.test(password)) score += 20
  
  // Bonus for length
  if (password.length >= 16) score += 10
  
  if (score < 40) {
    return { percentage: score, color: '#f56c6c', text: 'Weak' }
  } else if (score < 70) {
    return { percentage: score, color: '#e6a23c', text: 'Medium' }
  } else {
    return { percentage: score, color: '#67c23a', text: 'Strong' }
  }
})

// Input sanitization
const sanitizeInput = (field, value) => {
  // Remove potentially dangerous characters
  const sanitized = DOMPurify.sanitize(value, { 
    ALLOWED_TAGS: [],
    ALLOWED_ATTR: []
  })
  
  form[field] = sanitized
}

// Email validation
const validateEmail = async () => {
  if (!form.email) return
  
  try {
    // Check if email is already registered
    const response = await fetch('/api/auth/check-email', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCSRFToken()
      },
      body: JSON.stringify({ email: form.email })
    })
    
    const result = await response.json()
    
    if (result.exists) {
      ElMessage.warning('Email is already registered')
    }
  } catch (error) {
    console.error('Email validation error:', error)
  }
}

// Secure file validation
const secureFileValidation = (file) => {
  // File type validation
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
  if (!allowedTypes.includes(file.type)) {
    ElMessage.error('Only JPEG, PNG, and WebP images are allowed')
    return false
  }
  
  // File size validation (max 5MB)
  const maxSize = 5 * 1024 * 1024
  if (file.size > maxSize) {
    ElMessage.error('File size cannot exceed 5MB')
    return false
  }
  
  // File name validation
  const fileName = file.name
  if (!/^[a-zA-Z0-9._-]+$/.test(fileName)) {
    ElMessage.error('File name contains invalid characters')
    return false
  }
  
  // Check file signature (magic numbers)
  return validateFileSignature(file)
}

// Validate file signature to prevent file type spoofing
const validateFileSignature = (file) => {
  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.onload = (e) => {
      const arr = new Uint8Array(e.target.result).subarray(0, 4)
      let header = ''
      for (let i = 0; i < arr.length; i++) {
        header += arr[i].toString(16).padStart(2, '0')
      }
      
      // Check magic numbers for image files
      const validSignatures = {
        'ffd8ffe0': 'image/jpeg',
        'ffd8ffe1': 'image/jpeg',
        'ffd8ffe2': 'image/jpeg',
        '89504e47': 'image/png',
        '52494646': 'image/webp'
      }
      
      const isValid = Object.keys(validSignatures).some(signature => 
        header.startsWith(signature)
      )
      
      if (!isValid) {
        ElMessage.error('Invalid file format detected')
        resolve(false)
      } else {
        resolve(true)
      }
    }
    reader.readAsArrayBuffer(file.slice(0, 4))
  })
}

// Upload configuration
const uploadUrl = computed(() => {
  return `${import.meta.env.VITE_API_BASE_URL}/api/upload/avatar`
})

const uploadHeaders = computed(() => {
  return {
    'Authorization': `Bearer ${getAuthToken()}`,
    'X-CSRF-Token': getCSRFToken()
  }
})

// CAPTCHA management
const refreshCaptcha = async () => {
  try {
    const response = await fetch('/api/auth/captcha', {
      method: 'GET',
      headers: {
        'X-CSRF-Token': getCSRFToken()
      }
    })
    
    const result = await response.json()
    captchaUrl.value = result.imageUrl
  } catch (error) {
    ElMessage.error('Failed to load CAPTCHA')
  }
}

// Secure form submission
const handleSecureSubmit = async () => {
  if (!formRef.value) return
  
  try {
    // Validate form
    await formRef.value.validate()
    
    // Additional security validation
    const validationResult = validationSchema.safeParse(form)
    if (!validationResult.success) {
      ElMessage.error('Form validation failed')
      return
    }
    
    submitting.value = true
    
    // Prepare secure payload
    const payload = {
      ...form,
      timestamp: Date.now(),
      nonce: generateNonce()
    }
    
    // Submit with security headers
    const response = await fetch('/api/auth/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCSRFToken(),
        'X-Requested-With': 'XMLHttpRequest'
      },
      body: JSON.stringify(payload)
    })
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    
    const result = await response.json()
    
    if (result.success) {
      ElMessage.success('Registration successful')
      // Redirect or handle success
    } else {
      ElMessage.error(result.message || 'Registration failed')
    }
    
  } catch (error) {
    console.error('Submission error:', error)
    ElMessage.error('An error occurred during submission')
  } finally {
    submitting.value = false
  }
}

// Security utility functions
const getCSRFToken = () => {
  return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}

const getAuthToken = () => {
  return localStorage.getItem('auth_token') || ''
}

const generateNonce = () => {
  return Math.random().toString(36).substring(2, 15) + 
         Math.random().toString(36).substring(2, 15)
}

// Upload handlers
const handleUploadSuccess = (response, file) => {
  if (response.success) {
    form.avatar = response.url
    ElMessage.success('Avatar uploaded successfully')
  } else {
    ElMessage.error('Upload failed')
  }
}

const handleUploadError = (error) => {
  console.error('Upload error:', error)
  ElMessage.error('Upload failed')
}

onMounted(() => {
  refreshCaptcha()
})
</script>

<style scoped>
.password-strength {
  margin-top: 8px;
}

.strength-text {
  font-size: 12px;
  margin-left: 8px;
}

.captcha-container {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.captcha-container img {
  height: 32px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}
</style>

Authentication and Authorization

Secure Authentication Service

javascript
// services/auth.js
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import CryptoJS from 'crypto-js'

class AuthService {
  constructor() {
    this.user = ref(null)
    this.token = ref(localStorage.getItem('auth_token'))
    this.refreshToken = ref(localStorage.getItem('refresh_token'))
    this.tokenExpiry = ref(localStorage.getItem('token_expiry'))
    this.loginAttempts = ref(0)
    this.lockoutTime = ref(null)
    
    // Initialize auth state
    this.initializeAuth()
  }
  
  // Computed properties
  get isAuthenticated() {
    return computed(() => {
      return this.token.value && 
             this.tokenExpiry.value && 
             Date.now() < parseInt(this.tokenExpiry.value)
    })
  }
  
  get isLocked() {
    return computed(() => {
      return this.lockoutTime.value && Date.now() < this.lockoutTime.value
    })
  }
  
  // Initialize authentication state
  initializeAuth() {
    if (this.token.value) {
      this.validateToken()
    }
    
    // Set up token refresh timer
    this.setupTokenRefresh()
  }
  
  // Secure login with rate limiting
  async login(credentials) {
    try {
      // Check for account lockout
      if (this.isLocked.value) {
        const remainingTime = Math.ceil((this.lockoutTime.value - Date.now()) / 1000)
        throw new Error(`Account locked. Try again in ${remainingTime} seconds.`)
      }
      
      // Encrypt sensitive data
      const encryptedCredentials = this.encryptCredentials(credentials)
      
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': this.getCSRFToken(),
          'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({
          ...encryptedCredentials,
          timestamp: Date.now(),
          fingerprint: await this.generateFingerprint()
        })
      })
      
      const result = await response.json()
      
      if (!response.ok) {
        this.handleLoginFailure()
        throw new Error(result.message || 'Login failed')
      }
      
      // Successful login
      this.handleLoginSuccess(result)
      return result
      
    } catch (error) {
      console.error('Login error:', error)
      throw error
    }
  }
  
  // Handle successful login
  handleLoginSuccess(result) {
    this.user.value = result.user
    this.token.value = result.token
    this.refreshToken.value = result.refreshToken
    this.tokenExpiry.value = result.expiresAt
    
    // Store in secure storage
    this.storeTokens(result)
    
    // Reset login attempts
    this.loginAttempts.value = 0
    this.lockoutTime.value = null
    localStorage.removeItem('lockout_time')
    
    // Set up token refresh
    this.setupTokenRefresh()
    
    ElMessage.success('Login successful')
  }
  
  // Handle login failure with rate limiting
  handleLoginFailure() {
    this.loginAttempts.value++
    
    // Implement exponential backoff
    if (this.loginAttempts.value >= 5) {
      const lockoutDuration = Math.min(300000, Math.pow(2, this.loginAttempts.value - 5) * 60000) // Max 5 minutes
      this.lockoutTime.value = Date.now() + lockoutDuration
      localStorage.setItem('lockout_time', this.lockoutTime.value.toString())
    }
    
    localStorage.setItem('login_attempts', this.loginAttempts.value.toString())
  }
  
  // Encrypt credentials before transmission
  encryptCredentials(credentials) {
    const publicKey = this.getPublicKey()
    
    return {
      username: credentials.username, // Username can be plain text
      password: CryptoJS.AES.encrypt(credentials.password, publicKey).toString(),
      rememberMe: credentials.rememberMe
    }
  }
  
  // Generate device fingerprint
  async generateFingerprint() {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    ctx.textBaseline = 'top'
    ctx.font = '14px Arial'
    ctx.fillText('Device fingerprint', 2, 2)
    
    const fingerprint = {
      userAgent: navigator.userAgent,
      language: navigator.language,
      platform: navigator.platform,
      screen: `${screen.width}x${screen.height}`,
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      canvas: canvas.toDataURL()
    }
    
    return CryptoJS.SHA256(JSON.stringify(fingerprint)).toString()
  }
  
  // Secure token storage
  storeTokens(result) {
    // Use secure storage for sensitive data
    if (result.rememberMe) {
      localStorage.setItem('auth_token', result.token)
      localStorage.setItem('refresh_token', result.refreshToken)
      localStorage.setItem('token_expiry', result.expiresAt)
    } else {
      sessionStorage.setItem('auth_token', result.token)
      sessionStorage.setItem('refresh_token', result.refreshToken)
      sessionStorage.setItem('token_expiry', result.expiresAt)
    }
  }
  
  // Token refresh mechanism
  setupTokenRefresh() {
    if (!this.token.value || !this.tokenExpiry.value) return
    
    const expiryTime = parseInt(this.tokenExpiry.value)
    const refreshTime = expiryTime - (5 * 60 * 1000) // Refresh 5 minutes before expiry
    const timeUntilRefresh = refreshTime - Date.now()
    
    if (timeUntilRefresh > 0) {
      setTimeout(() => {
        this.refreshAuthToken()
      }, timeUntilRefresh)
    }
  }
  
  // Refresh authentication token
  async refreshAuthToken() {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.refreshToken.value}`,
          'X-CSRF-Token': this.getCSRFToken()
        }
      })
      
      if (!response.ok) {
        throw new Error('Token refresh failed')
      }
      
      const result = await response.json()
      
      this.token.value = result.token
      this.tokenExpiry.value = result.expiresAt
      
      // Update storage
      this.updateTokenStorage(result)
      
      // Set up next refresh
      this.setupTokenRefresh()
      
    } catch (error) {
      console.error('Token refresh error:', error)
      this.logout()
    }
  }
  
  // Update token in storage
  updateTokenStorage(result) {
    const storage = localStorage.getItem('auth_token') ? localStorage : sessionStorage
    storage.setItem('auth_token', result.token)
    storage.setItem('token_expiry', result.expiresAt)
  }
  
  // Validate existing token
  async validateToken() {
    try {
      const response = await fetch('/api/auth/validate', {
        method: 'GET',
        headers: {
          'Authorization': `Bearer ${this.token.value}`,
          'X-CSRF-Token': this.getCSRFToken()
        }
      })
      
      if (!response.ok) {
        throw new Error('Token validation failed')
      }
      
      const result = await response.json()
      this.user.value = result.user
      
    } catch (error) {
      console.error('Token validation error:', error)
      this.logout()
    }
  }
  
  // Secure logout
  async logout() {
    try {
      // Notify server of logout
      if (this.token.value) {
        await fetch('/api/auth/logout', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.token.value}`,
            'X-CSRF-Token': this.getCSRFToken()
          }
        })
      }
    } catch (error) {
      console.error('Logout error:', error)
    } finally {
      // Clear local state
      this.clearAuthState()
    }
  }
  
  // Clear authentication state
  clearAuthState() {
    this.user.value = null
    this.token.value = null
    this.refreshToken.value = null
    this.tokenExpiry.value = null
    
    // Clear storage
    localStorage.removeItem('auth_token')
    localStorage.removeItem('refresh_token')
    localStorage.removeItem('token_expiry')
    sessionStorage.removeItem('auth_token')
    sessionStorage.removeItem('refresh_token')
    sessionStorage.removeItem('token_expiry')
    
    ElMessage.info('Logged out successfully')
  }
  
  // Check user permissions
  hasPermission(permission) {
    if (!this.user.value || !this.user.value.permissions) {
      return false
    }
    
    return this.user.value.permissions.includes(permission)
  }
  
  // Check user role
  hasRole(role) {
    if (!this.user.value || !this.user.value.roles) {
      return false
    }
    
    return this.user.value.roles.includes(role)
  }
  
  // Utility methods
  getCSRFToken() {
    return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
  }
  
  getPublicKey() {
    return document.querySelector('meta[name="public-key"]')?.getAttribute('content') || 'default-key'
  }
}

// Export singleton instance
export const authService = new AuthService()
export default authService

Role-Based Access Control

vue
<template>
  <div class="admin-panel">
    <!-- Role-based navigation -->
    <el-menu
      :default-active="activeIndex"
      mode="horizontal"
      @select="handleSelect"
    >
      <el-menu-item index="dashboard">
        Dashboard
      </el-menu-item>
      
      <!-- Admin only -->
      <el-menu-item 
        v-if="hasRole('admin')"
        index="users"
      >
        User Management
      </el-menu-item>
      
      <!-- Manager and above -->
      <el-menu-item 
        v-if="hasAnyRole(['admin', 'manager'])"
        index="reports"
      >
        Reports
      </el-menu-item>
      
      <!-- Permission-based access -->
      <el-menu-item 
        v-if="hasPermission('view_analytics')"
        index="analytics"
      >
        Analytics
      </el-menu-item>
    </el-menu>
    
    <!-- Protected content areas -->
    <div class="content-area">
      <!-- User management (Admin only) -->
      <ProtectedComponent 
        v-if="activeIndex === 'users'"
        :required-role="'admin'"
      >
        <UserManagement />
      </ProtectedComponent>
      
      <!-- Reports (Manager and above) -->
      <ProtectedComponent 
        v-if="activeIndex === 'reports'"
        :required-roles="['admin', 'manager']"
      >
        <ReportsPanel />
      </ProtectedComponent>
      
      <!-- Analytics (Permission-based) -->
      <ProtectedComponent 
        v-if="activeIndex === 'analytics'"
        :required-permission="'view_analytics'"
      >
        <AnalyticsDashboard />
      </ProtectedComponent>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useAuth } from '@/composables/useAuth'
import ProtectedComponent from '@/components/ProtectedComponent.vue'
import UserManagement from '@/components/UserManagement.vue'
import ReportsPanel from '@/components/ReportsPanel.vue'
import AnalyticsDashboard from '@/components/AnalyticsDashboard.vue'

const { user, hasRole, hasPermission, hasAnyRole } = useAuth()
const activeIndex = ref('dashboard')

const handleSelect = (index) => {
  activeIndex.value = index
}
</script>
vue
<!-- ProtectedComponent.vue -->
<template>
  <div v-if="hasAccess" class="protected-content">
    <slot />
  </div>
  <div v-else class="access-denied">
    <el-result
      icon="warning"
      title="Access Denied"
      sub-title="You don't have permission to view this content."
    >
      <template #extra>
        <el-button type="primary" @click="$router.push('/dashboard')">
          Back to Dashboard
        </el-button>
      </template>
    </el-result>
  </div>
</template>

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

const props = defineProps({
  requiredRole: {
    type: String,
    default: null
  },
  requiredRoles: {
    type: Array,
    default: () => []
  },
  requiredPermission: {
    type: String,
    default: null
  },
  requiredPermissions: {
    type: Array,
    default: () => []
  }
})

const { hasRole, hasPermission, hasAnyRole, hasAllPermissions } = useAuth()

const hasAccess = computed(() => {
  // Check single role
  if (props.requiredRole && !hasRole(props.requiredRole)) {
    return false
  }
  
  // Check multiple roles (any)
  if (props.requiredRoles.length > 0 && !hasAnyRole(props.requiredRoles)) {
    return false
  }
  
  // Check single permission
  if (props.requiredPermission && !hasPermission(props.requiredPermission)) {
    return false
  }
  
  // Check multiple permissions (all)
  if (props.requiredPermissions.length > 0 && !hasAllPermissions(props.requiredPermissions)) {
    return false
  }
  
  return true
})
</script>

Data Protection and Privacy

Secure Data Handling

javascript
// utils/data-protection.js
import CryptoJS from 'crypto-js'

class DataProtection {
  constructor() {
    this.encryptionKey = this.getEncryptionKey()
  }
  
  // Get encryption key from secure source
  getEncryptionKey() {
    // In production, this should come from a secure key management service
    return process.env.VUE_APP_ENCRYPTION_KEY || 'default-key'
  }
  
  // Encrypt sensitive data
  encrypt(data) {
    try {
      const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), this.encryptionKey).toString()
      return encrypted
    } catch (error) {
      console.error('Encryption error:', error)
      throw new Error('Failed to encrypt data')
    }
  }
  
  // Decrypt sensitive data
  decrypt(encryptedData) {
    try {
      const bytes = CryptoJS.AES.decrypt(encryptedData, this.encryptionKey)
      const decrypted = bytes.toString(CryptoJS.enc.Utf8)
      return JSON.parse(decrypted)
    } catch (error) {
      console.error('Decryption error:', error)
      throw new Error('Failed to decrypt data')
    }
  }
  
  // Hash sensitive data (one-way)
  hash(data) {
    return CryptoJS.SHA256(data).toString()
  }
  
  // Mask sensitive data for display
  maskData(data, type = 'default') {
    if (!data) return ''
    
    switch (type) {
      case 'email':
        const [username, domain] = data.split('@')
        return `${username.substring(0, 2)}***@${domain}`
      
      case 'phone':
        return data.replace(/(\d{3})(\d{4})(\d{4})/, '$1-****-$3')
      
      case 'creditCard':
        return data.replace(/(\d{4})(\d{4})(\d{4})(\d{4})/, '**** **** **** $4')
      
      case 'ssn':
        return data.replace(/(\d{3})(\d{2})(\d{4})/, '***-**-$3')
      
      default:
        return '*'.repeat(data.length)
    }
  }
  
  // Sanitize data for safe display
  sanitize(data) {
    if (typeof data !== 'string') return data
    
    return data
      .replace(/[<>"'&]/g, (match) => {
        const entities = {
          '<': '&lt;',
          '>': '&gt;',
          '"': '&quot;',
          "'": '&#x27;',
          '&': '&amp;'
        }
        return entities[match]
      })
  }
  
  // Generate secure random token
  generateSecureToken(length = 32) {
    const array = new Uint8Array(length)
    crypto.getRandomValues(array)
    return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
  }
  
  // Validate data integrity
  validateIntegrity(data, expectedHash) {
    const actualHash = this.hash(JSON.stringify(data))
    return actualHash === expectedHash
  }
}

export const dataProtection = new DataProtection()
export default dataProtection

Privacy-Compliant Data Collection

vue
<template>
  <div class="privacy-settings">
    <el-card>
      <template #header>
        <div class="card-header">
          <span>Privacy Settings</span>
          <el-tag type="info">GDPR Compliant</el-tag>
        </div>
      </template>
      
      <!-- Consent management -->
      <el-form :model="privacySettings" label-width="200px">
        <el-form-item label="Essential Cookies">
          <el-switch 
            v-model="privacySettings.essential" 
            disabled
          />
          <div class="setting-description">
            Required for basic website functionality. Cannot be disabled.
          </div>
        </el-form-item>
        
        <el-form-item label="Analytics Cookies">
          <el-switch 
            v-model="privacySettings.analytics"
            @change="updateConsent('analytics', $event)"
          />
          <div class="setting-description">
            Help us understand how you use our website to improve user experience.
          </div>
        </el-form-item>
        
        <el-form-item label="Marketing Cookies">
          <el-switch 
            v-model="privacySettings.marketing"
            @change="updateConsent('marketing', $event)"
          />
          <div class="setting-description">
            Used to show you relevant advertisements based on your interests.
          </div>
        </el-form-item>
        
        <el-form-item label="Personalization">
          <el-switch 
            v-model="privacySettings.personalization"
            @change="updateConsent('personalization', $event)"
          />
          <div class="setting-description">
            Customize content and features based on your preferences.
          </div>
        </el-form-item>
      </el-form>
      
      <el-divider />
      
      <!-- Data management -->
      <h3>Data Management</h3>
      
      <el-space direction="vertical" style="width: 100%">
        <el-button 
          type="primary" 
          @click="downloadData"
          :loading="downloading"
        >
          <el-icon><Download /></el-icon>
          Download My Data
        </el-button>
        
        <el-button 
          type="warning" 
          @click="requestDataDeletion"
          :loading="deleting"
        >
          <el-icon><Delete /></el-icon>
          Request Data Deletion
        </el-button>
        
        <el-button 
          type="info" 
          @click="viewDataUsage"
        >
          <el-icon><View /></el-icon>
          View Data Usage
        </el-button>
      </el-space>
    </el-card>
    
    <!-- Data usage modal -->
    <el-dialog
      v-model="dataUsageVisible"
      title="Data Usage Information"
      width="600px"
    >
      <el-table :data="dataUsageInfo" style="width: 100%">
        <el-table-column prop="category" label="Data Category" />
        <el-table-column prop="purpose" label="Purpose" />
        <el-table-column prop="retention" label="Retention Period" />
        <el-table-column prop="sharing" label="Third-party Sharing" />
      </el-table>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { usePrivacy } from '@/composables/usePrivacy'

const { 
  getConsentSettings, 
  updateConsentSettings, 
  downloadUserData, 
  requestDataDeletion 
} = usePrivacy()

const downloading = ref(false)
const deleting = ref(false)
const dataUsageVisible = ref(false)

const privacySettings = reactive({
  essential: true,
  analytics: false,
  marketing: false,
  personalization: false
})

const dataUsageInfo = ref([
  {
    category: 'Account Information',
    purpose: 'User authentication and account management',
    retention: '5 years after account deletion',
    sharing: 'No'
  },
  {
    category: 'Usage Analytics',
    purpose: 'Improve website performance and user experience',
    retention: '2 years',
    sharing: 'Anonymized data only'
  },
  {
    category: 'Marketing Data',
    purpose: 'Personalized advertisements and communications',
    retention: '1 year or until consent withdrawn',
    sharing: 'Trusted partners only'
  }
])

// Update consent settings
const updateConsent = async (type, value) => {
  try {
    await updateConsentSettings({ [type]: value })
    ElMessage.success('Privacy settings updated')
    
    // Update tracking scripts based on consent
    updateTrackingScripts(type, value)
    
  } catch (error) {
    console.error('Failed to update consent:', error)
    ElMessage.error('Failed to update privacy settings')
    
    // Revert the change
    privacySettings[type] = !value
  }
}

// Download user data (GDPR Article 20)
const downloadData = async () => {
  try {
    downloading.value = true
    
    const data = await downloadUserData()
    
    // Create and download file
    const blob = new Blob([JSON.stringify(data, null, 2)], {
      type: 'application/json'
    })
    
    const url = URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.download = `user-data-${new Date().toISOString().split('T')[0]}.json`
    link.click()
    
    URL.revokeObjectURL(url)
    
    ElMessage.success('Data download started')
    
  } catch (error) {
    console.error('Data download error:', error)
    ElMessage.error('Failed to download data')
  } finally {
    downloading.value = false
  }
}

// Request data deletion (GDPR Article 17)
const requestDataDeletion = async () => {
  try {
    await ElMessageBox.confirm(
      'This will permanently delete all your data. This action cannot be undone. Are you sure?',
      'Confirm Data Deletion',
      {
        confirmButtonText: 'Delete My Data',
        cancelButtonText: 'Cancel',
        type: 'warning',
        confirmButtonClass: 'el-button--danger'
      }
    )
    
    deleting.value = true
    
    await requestDataDeletion()
    
    ElMessage.success('Data deletion request submitted. You will receive a confirmation email.')
    
  } catch (error) {
    if (error !== 'cancel') {
      console.error('Data deletion error:', error)
      ElMessage.error('Failed to submit deletion request')
    }
  } finally {
    deleting.value = false
  }
}

// View data usage information
const viewDataUsage = () => {
  dataUsageVisible.value = true
}

// Update tracking scripts based on consent
const updateTrackingScripts = (type, enabled) => {
  switch (type) {
    case 'analytics':
      if (enabled) {
        // Load analytics script
        loadScript('https://www.google-analytics.com/analytics.js')
      } else {
        // Disable analytics
        if (window.gtag) {
          window.gtag('consent', 'update', {
            'analytics_storage': 'denied'
          })
        }
      }
      break
      
    case 'marketing':
      if (enabled) {
        // Load marketing scripts
        loadScript('https://connect.facebook.net/en_US/fbevents.js')
      } else {
        // Disable marketing tracking
        if (window.gtag) {
          window.gtag('consent', 'update', {
            'ad_storage': 'denied'
          })
        }
      }
      break
  }
}

// Utility function to load scripts
const loadScript = (src) => {
  const script = document.createElement('script')
  script.src = src
  script.async = true
  document.head.appendChild(script)
}

// Load current settings on mount
onMounted(async () => {
  try {
    const settings = await getConsentSettings()
    Object.assign(privacySettings, settings)
  } catch (error) {
    console.error('Failed to load privacy settings:', error)
  }
})
</script>

<style scoped>
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.setting-description {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
}
</style>

Security Headers and CSP

Content Security Policy Configuration

javascript
// vite.config.js - Security headers configuration
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'security-headers',
      configureServer(server) {
        server.middlewares.use((req, res, next) => {
          // Content Security Policy
          res.setHeader('Content-Security-Policy', [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com",
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
            "font-src 'self' https://fonts.gstatic.com",
            "img-src 'self' data: https: blob:",
            "connect-src 'self' https://api.example.com",
            "frame-src 'none'",
            "object-src 'none'",
            "base-uri 'self'",
            "form-action 'self'"
          ].join('; '))
          
          // Other security headers
          res.setHeader('X-Content-Type-Options', 'nosniff')
          res.setHeader('X-Frame-Options', 'DENY')
          res.setHeader('X-XSS-Protection', '1; mode=block')
          res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
          res.setHeader('Permissions-Policy', [
            'camera=()',
            'microphone=()',
            'geolocation=()',
            'payment=()'
          ].join(', '))
          
          // HSTS (only in production)
          if (process.env.NODE_ENV === 'production') {
            res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
          }
          
          next()
        })
      }
    }
  ]
})

Security Monitoring

javascript
// utils/security-monitor.js
class SecurityMonitor {
  constructor() {
    this.violations = []
    this.setupCSPReporting()
    this.setupSecurityEventListeners()
  }
  
  // Set up CSP violation reporting
  setupCSPReporting() {
    document.addEventListener('securitypolicyviolation', (event) => {
      const violation = {
        type: 'csp_violation',
        directive: event.violatedDirective,
        blockedURI: event.blockedURI,
        sourceFile: event.sourceFile,
        lineNumber: event.lineNumber,
        timestamp: Date.now()
      }
      
      this.reportViolation(violation)
    })
  }
  
  // Set up security event listeners
  setupSecurityEventListeners() {
    // Detect potential XSS attempts
    this.monitorDOMChanges()
    
    // Monitor for suspicious network requests
    this.monitorNetworkRequests()
    
    // Detect potential clickjacking
    this.monitorFrameAccess()
  }
  
  // Monitor DOM changes for potential XSS
  monitorDOMChanges() {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              this.checkForSuspiciousContent(node)
            }
          })
        }
      })
    })
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    })
  }
  
  // Check for suspicious content
  checkForSuspiciousContent(element) {
    const suspiciousPatterns = [
      /<script[^>]*>.*?<\/script>/gi,
      /javascript:/gi,
      /on\w+\s*=/gi,
      /eval\s*\(/gi
    ]
    
    const content = element.innerHTML
    
    suspiciousPatterns.forEach((pattern) => {
      if (pattern.test(content)) {
        this.reportViolation({
          type: 'potential_xss',
          element: element.tagName,
          content: content.substring(0, 100),
          timestamp: Date.now()
        })
      }
    })
  }
  
  // Monitor network requests
  monitorNetworkRequests() {
    const originalFetch = window.fetch
    
    window.fetch = async (...args) => {
      const [url, options] = args
      
      // Check for suspicious requests
      if (this.isSuspiciousRequest(url, options)) {
        this.reportViolation({
          type: 'suspicious_request',
          url: url.toString(),
          method: options?.method || 'GET',
          timestamp: Date.now()
        })
      }
      
      return originalFetch.apply(this, args)
    }
  }
  
  // Check if request is suspicious
  isSuspiciousRequest(url, options) {
    const urlString = url.toString()
    
    // Check for external domains not in whitelist
    const allowedDomains = [
      window.location.hostname,
      'api.example.com',
      'cdn.jsdelivr.net'
    ]
    
    try {
      const requestUrl = new URL(urlString)
      if (!allowedDomains.includes(requestUrl.hostname)) {
        return true
      }
    } catch (error) {
      return true // Invalid URL
    }
    
    return false
  }
  
  // Monitor frame access attempts
  monitorFrameAccess() {
    try {
      if (window.top !== window.self) {
        this.reportViolation({
          type: 'frame_access',
          message: 'Page loaded in frame',
          timestamp: Date.now()
        })
      }
    } catch (error) {
      // Cross-origin frame access blocked (good)
    }
  }
  
  // Report security violation
  reportViolation(violation) {
    this.violations.push(violation)
    
    // Send to security monitoring service
    this.sendToSecurityService(violation)
    
    // Log locally for debugging
    console.warn('Security violation detected:', violation)
  }
  
  // Send violation to security service
  async sendToSecurityService(violation) {
    try {
      await fetch('/api/security/violations', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': this.getCSRFToken()
        },
        body: JSON.stringify({
          violation,
          userAgent: navigator.userAgent,
          url: window.location.href,
          timestamp: Date.now()
        })
      })
    } catch (error) {
      console.error('Failed to report security violation:', error)
    }
  }
  
  // Get CSRF token
  getCSRFToken() {
    return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
  }
  
  // Get violation summary
  getViolationSummary() {
    const summary = {}
    
    this.violations.forEach((violation) => {
      summary[violation.type] = (summary[violation.type] || 0) + 1
    })
    
    return summary
  }
}

// Initialize security monitoring
export const securityMonitor = new SecurityMonitor()
export default securityMonitor

Best Practices Summary

1. Input Validation

  • Validate all user inputs on both client and server side
  • Use schema validation libraries like Zod
  • Sanitize data before display using DOMPurify
  • Implement proper file upload validation

2. Authentication & Authorization

  • Implement secure login with rate limiting
  • Use strong password policies
  • Implement proper session management
  • Use role-based access control (RBAC)

3. Data Protection

  • Encrypt sensitive data in transit and at rest
  • Implement proper key management
  • Mask sensitive data in UI
  • Comply with privacy regulations (GDPR, CCPA)

4. Security Headers

  • Implement Content Security Policy (CSP)
  • Use security headers (HSTS, X-Frame-Options, etc.)
  • Monitor and report security violations
  • Regular security audits and updates

5. Secure Development

  • Follow secure coding practices
  • Regular dependency updates
  • Implement security monitoring
  • Conduct security testing

By following these security best practices, you can build robust and secure Element Plus applications that protect user data and prevent common security vulnerabilities.

Element Plus Study Guide