Skip to content

Security Best Practices for Element Plus Applications

Overview

This guide covers comprehensive security best practices for Element Plus applications, including authentication, authorization, data protection, secure coding practices, and vulnerability management.

Authentication and Authorization

JWT Token Management

typescript
// src/utils/auth/token-manager.ts
import { jwtDecode } from 'jwt-decode'
import CryptoJS from 'crypto-js'

export interface TokenPayload {
  sub: string
  iat: number
  exp: number
  roles: string[]
  permissions: string[]
  sessionId: string
}

export class TokenManager {
  private static readonly ACCESS_TOKEN_KEY = 'access_token'
  private static readonly REFRESH_TOKEN_KEY = 'refresh_token'
  private static readonly ENCRYPTION_KEY = process.env.VITE_TOKEN_ENCRYPTION_KEY || 'default-key'
  private static readonly TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000 // 5 minutes
  
  private static refreshPromise: Promise<string> | null = null
  
  /**
   * Securely store encrypted token
   */
  public static setTokens(accessToken: string, refreshToken: string): void {
    try {
      const encryptedAccessToken = this.encryptToken(accessToken)
      const encryptedRefreshToken = this.encryptToken(refreshToken)
      
      // Use secure storage with httpOnly flag simulation
      this.setSecureItem(this.ACCESS_TOKEN_KEY, encryptedAccessToken)
      this.setSecureItem(this.REFRESH_TOKEN_KEY, encryptedRefreshToken)
      
      // Set automatic refresh
      this.scheduleTokenRefresh(accessToken)
    } catch (error) {
      console.error('Failed to store tokens:', error)
      throw new Error('Token storage failed')
    }
  }
  
  /**
   * Get decrypted access token
   */
  public static getAccessToken(): string | null {
    try {
      const encryptedToken = this.getSecureItem(this.ACCESS_TOKEN_KEY)
      if (!encryptedToken) return null
      
      const token = this.decryptToken(encryptedToken)
      
      // Validate token before returning
      if (this.isTokenValid(token)) {
        return token
      }
      
      // Token is invalid, attempt refresh
      this.refreshTokenIfNeeded()
      return null
    } catch (error) {
      console.error('Failed to retrieve access token:', error)
      this.clearTokens()
      return null
    }
  }
  
  /**
   * Get decrypted refresh token
   */
  public static getRefreshToken(): string | null {
    try {
      const encryptedToken = this.getSecureItem(this.REFRESH_TOKEN_KEY)
      if (!encryptedToken) return null
      
      return this.decryptToken(encryptedToken)
    } catch (error) {
      console.error('Failed to retrieve refresh token:', error)
      this.clearTokens()
      return null
    }
  }
  
  /**
   * Clear all tokens
   */
  public static clearTokens(): void {
    this.removeSecureItem(this.ACCESS_TOKEN_KEY)
    this.removeSecureItem(this.REFRESH_TOKEN_KEY)
    
    // Clear any scheduled refresh
    if (this.refreshTimeout) {
      clearTimeout(this.refreshTimeout)
      this.refreshTimeout = null
    }
  }
  
  /**
   * Validate token structure and expiration
   */
  public static isTokenValid(token: string): boolean {
    try {
      const decoded = jwtDecode<TokenPayload>(token)
      const now = Date.now() / 1000
      
      // Check expiration with buffer
      return decoded.exp > now + 60 // 1 minute buffer
    } catch (error) {
      return false
    }
  }
  
  /**
   * Get token payload
   */
  public static getTokenPayload(): TokenPayload | null {
    const token = this.getAccessToken()
    if (!token) return null
    
    try {
      return jwtDecode<TokenPayload>(token)
    } catch (error) {
      console.error('Failed to decode token:', error)
      return null
    }
  }
  
  /**
   * Check if token needs refresh
   */
  public static shouldRefreshToken(): boolean {
    const token = this.getAccessToken()
    if (!token) return true
    
    try {
      const decoded = jwtDecode<TokenPayload>(token)
      const now = Date.now() / 1000
      const timeUntilExpiry = (decoded.exp - now) * 1000
      
      return timeUntilExpiry <= this.TOKEN_REFRESH_THRESHOLD
    } catch (error) {
      return true
    }
  }
  
  /**
   * Refresh token if needed
   */
  public static async refreshTokenIfNeeded(): Promise<string | null> {
    if (!this.shouldRefreshToken()) {
      return this.getAccessToken()
    }
    
    // Prevent multiple simultaneous refresh attempts
    if (this.refreshPromise) {
      return this.refreshPromise
    }
    
    this.refreshPromise = this.performTokenRefresh()
    
    try {
      const newToken = await this.refreshPromise
      return newToken
    } finally {
      this.refreshPromise = null
    }
  }
  
  private static async performTokenRefresh(): Promise<string | null> {
    const refreshToken = this.getRefreshToken()
    if (!refreshToken) {
      this.clearTokens()
      throw new Error('No refresh token available')
    }
    
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${refreshToken}`
        },
        credentials: 'same-origin'
      })
      
      if (!response.ok) {
        throw new Error('Token refresh failed')
      }
      
      const data = await response.json()
      this.setTokens(data.accessToken, data.refreshToken)
      
      return data.accessToken
    } catch (error) {
      console.error('Token refresh failed:', error)
      this.clearTokens()
      
      // Emit logout event
      window.dispatchEvent(new CustomEvent('auth:logout', {
        detail: { reason: 'token_refresh_failed' }
      }))
      
      throw error
    }
  }
  
  private static refreshTimeout: NodeJS.Timeout | null = null
  
  private static scheduleTokenRefresh(token: string): void {
    try {
      const decoded = jwtDecode<TokenPayload>(token)
      const now = Date.now() / 1000
      const timeUntilRefresh = Math.max(
        (decoded.exp - now - this.TOKEN_REFRESH_THRESHOLD / 1000) * 1000,
        0
      )
      
      if (this.refreshTimeout) {
        clearTimeout(this.refreshTimeout)
      }
      
      this.refreshTimeout = setTimeout(() => {
        this.refreshTokenIfNeeded()
      }, timeUntilRefresh)
    } catch (error) {
      console.error('Failed to schedule token refresh:', error)
    }
  }
  
  private static encryptToken(token: string): string {
    return CryptoJS.AES.encrypt(token, this.ENCRYPTION_KEY).toString()
  }
  
  private static decryptToken(encryptedToken: string): string {
    const bytes = CryptoJS.AES.decrypt(encryptedToken, this.ENCRYPTION_KEY)
    return bytes.toString(CryptoJS.enc.Utf8)
  }
  
  private static setSecureItem(key: string, value: string): void {
    // In a real application, consider using secure storage
    // For now, using localStorage with encryption
    localStorage.setItem(key, value)
  }
  
  private static getSecureItem(key: string): string | null {
    return localStorage.getItem(key)
  }
  
  private static removeSecureItem(key: string): void {
    localStorage.removeItem(key)
  }
}

Role-Based Access Control (RBAC)

typescript
// src/utils/auth/rbac.ts
import { TokenManager } from './token-manager'

export interface Permission {
  resource: string
  action: string
  conditions?: Record<string, any>
}

export interface Role {
  name: string
  permissions: Permission[]
  inherits?: string[]
}

export class RBACManager {
  private static roles: Map<string, Role> = new Map()
  private static userRoles: string[] = []
  private static userPermissions: Permission[] = []
  
  /**
   * Initialize RBAC with user roles and permissions
   */
  public static initialize(): void {
    const payload = TokenManager.getTokenPayload()
    if (payload) {
      this.userRoles = payload.roles || []
      this.loadUserPermissions()
    }
  }
  
  /**
   * Check if user has specific permission
   */
  public static hasPermission(
    resource: string,
    action: string,
    context?: Record<string, any>
  ): boolean {
    return this.userPermissions.some(permission => {
      if (permission.resource !== resource || permission.action !== action) {
        return false
      }
      
      // Check conditions if provided
      if (permission.conditions && context) {
        return this.evaluateConditions(permission.conditions, context)
      }
      
      return true
    })
  }
  
  /**
   * Check if user has any of the specified roles
   */
  public static hasRole(roles: string | string[]): boolean {
    const rolesToCheck = Array.isArray(roles) ? roles : [roles]
    return rolesToCheck.some(role => this.userRoles.includes(role))
  }
  
  /**
   * Check if user has all specified roles
   */
  public static hasAllRoles(roles: string[]): boolean {
    return roles.every(role => this.userRoles.includes(role))
  }
  
  /**
   * Get user's effective permissions
   */
  public static getUserPermissions(): Permission[] {
    return [...this.userPermissions]
  }
  
  /**
   * Get user's roles
   */
  public static getUserRoles(): string[] {
    return [...this.userRoles]
  }
  
  /**
   * Check if user can access a route
   */
  public static canAccessRoute(routeMeta: any): boolean {
    // Check authentication requirement
    if (routeMeta.requiresAuth && !TokenManager.getAccessToken()) {
      return false
    }
    
    // Check role requirements
    if (routeMeta.roles && !this.hasRole(routeMeta.roles)) {
      return false
    }
    
    // Check permission requirements
    if (routeMeta.permissions) {
      return routeMeta.permissions.every((perm: any) => 
        this.hasPermission(perm.resource, perm.action, perm.context)
      )
    }
    
    return true
  }
  
  private static loadUserPermissions(): void {
    this.userPermissions = []
    
    // Load permissions from token payload
    const payload = TokenManager.getTokenPayload()
    if (payload?.permissions) {
      this.userPermissions = payload.permissions.map(perm => {
        const [resource, action] = perm.split(':')
        return { resource, action }
      })
    }
    
    // Load role-based permissions
    this.userRoles.forEach(roleName => {
      const role = this.roles.get(roleName)
      if (role) {
        this.userPermissions.push(...role.permissions)
        
        // Handle role inheritance
        if (role.inherits) {
          role.inherits.forEach(inheritedRole => {
            const inherited = this.roles.get(inheritedRole)
            if (inherited) {
              this.userPermissions.push(...inherited.permissions)
            }
          })
        }
      }
    })
    
    // Remove duplicates
    this.userPermissions = this.userPermissions.filter(
      (perm, index, self) => 
        index === self.findIndex(p => 
          p.resource === perm.resource && p.action === perm.action
        )
    )
  }
  
  private static evaluateConditions(
    conditions: Record<string, any>,
    context: Record<string, any>
  ): boolean {
    return Object.entries(conditions).every(([key, value]) => {
      const contextValue = context[key]
      
      if (typeof value === 'object' && value !== null) {
        // Handle operators like { $eq: 'value' }, { $in: ['val1', 'val2'] }
        return Object.entries(value).every(([operator, operandValue]) => {
          switch (operator) {
            case '$eq':
              return contextValue === operandValue
            case '$ne':
              return contextValue !== operandValue
            case '$in':
              return Array.isArray(operandValue) && operandValue.includes(contextValue)
            case '$nin':
              return Array.isArray(operandValue) && !operandValue.includes(contextValue)
            case '$gt':
              return contextValue > operandValue
            case '$gte':
              return contextValue >= operandValue
            case '$lt':
              return contextValue < operandValue
            case '$lte':
              return contextValue <= operandValue
            default:
              return false
          }
        })
      }
      
      return contextValue === value
    })
  }
  
  /**
   * Register role definitions
   */
  public static registerRoles(roles: Role[]): void {
    roles.forEach(role => {
      this.roles.set(role.name, role)
    })
  }
}

// Vue directive for permission-based rendering
export const vPermission = {
  mounted(el: HTMLElement, binding: any) {
    const { resource, action, context } = binding.value
    
    if (!RBACManager.hasPermission(resource, action, context)) {
      el.style.display = 'none'
      el.setAttribute('data-permission-hidden', 'true')
    }
  },
  
  updated(el: HTMLElement, binding: any) {
    const { resource, action, context } = binding.value
    
    if (!RBACManager.hasPermission(resource, action, context)) {
      el.style.display = 'none'
      el.setAttribute('data-permission-hidden', 'true')
    } else {
      el.style.display = ''
      el.removeAttribute('data-permission-hidden')
    }
  }
}

// Vue directive for role-based rendering
export const vRole = {
  mounted(el: HTMLElement, binding: any) {
    const roles = Array.isArray(binding.value) ? binding.value : [binding.value]
    
    if (!RBACManager.hasRole(roles)) {
      el.style.display = 'none'
      el.setAttribute('data-role-hidden', 'true')
    }
  },
  
  updated(el: HTMLElement, binding: any) {
    const roles = Array.isArray(binding.value) ? binding.value : [binding.value]
    
    if (!RBACManager.hasRole(roles)) {
      el.style.display = 'none'
      el.setAttribute('data-role-hidden', 'true')
    } else {
      el.style.display = ''
      el.removeAttribute('data-role-hidden')
    }
  }
}

Input Validation and Sanitization

Secure Form Validation

typescript
// src/utils/validation/secure-validator.ts
import DOMPurify from 'dompurify'
import validator from 'validator'

export interface ValidationRule {
  required?: boolean
  type?: 'string' | 'number' | 'email' | 'url' | 'phone' | 'date' | 'boolean'
  minLength?: number
  maxLength?: number
  min?: number
  max?: number
  pattern?: RegExp
  custom?: (value: any) => boolean | string
  sanitize?: boolean
  allowedTags?: string[]
  allowedAttributes?: Record<string, string[]>
}

export interface ValidationResult {
  isValid: boolean
  errors: string[]
  sanitizedValue?: any
}

export class SecureValidator {
  private static readonly XSS_PATTERNS = [
    /<script[^>]*>.*?<\/script>/gi,
    /<iframe[^>]*>.*?<\/iframe>/gi,
    /javascript:/gi,
    /on\w+\s*=/gi,
    /<object[^>]*>.*?<\/object>/gi,
    /<embed[^>]*>.*?<\/embed>/gi,
    /<link[^>]*>/gi,
    /<meta[^>]*>/gi
  ]
  
  private static readonly SQL_INJECTION_PATTERNS = [
    /('|(\-\-)|(;)|(\||\|)|(\*|\*))/gi,
    /(exec(\s|\+)+(s|x)p\w+)/gi,
    /union[\s\w]*select/gi,
    /select[\s\w]*from/gi,
    /insert[\s\w]*into/gi,
    /delete[\s\w]*from/gi,
    /update[\s\w]*set/gi,
    /drop[\s\w]*table/gi
  ]
  
  /**
   * Validate and sanitize input value
   */
  public static validate(value: any, rules: ValidationRule): ValidationResult {
    const errors: string[] = []
    let sanitizedValue = value
    
    // Required validation
    if (rules.required && this.isEmpty(value)) {
      errors.push('This field is required')
      return { isValid: false, errors }
    }
    
    // Skip further validation if value is empty and not required
    if (this.isEmpty(value)) {
      return { isValid: true, errors: [], sanitizedValue: value }
    }
    
    // Sanitize input if requested
    if (rules.sanitize) {
      sanitizedValue = this.sanitizeInput(value, rules)
    }
    
    // Type validation
    if (rules.type) {
      const typeValidation = this.validateType(sanitizedValue, rules.type)
      if (!typeValidation.isValid) {
        errors.push(...typeValidation.errors)
      }
    }
    
    // Length validation for strings
    if (typeof sanitizedValue === 'string') {
      if (rules.minLength && sanitizedValue.length < rules.minLength) {
        errors.push(`Minimum length is ${rules.minLength} characters`)
      }
      
      if (rules.maxLength && sanitizedValue.length > rules.maxLength) {
        errors.push(`Maximum length is ${rules.maxLength} characters`)
      }
    }
    
    // Numeric range validation
    if (typeof sanitizedValue === 'number') {
      if (rules.min !== undefined && sanitizedValue < rules.min) {
        errors.push(`Minimum value is ${rules.min}`)
      }
      
      if (rules.max !== undefined && sanitizedValue > rules.max) {
        errors.push(`Maximum value is ${rules.max}`)
      }
    }
    
    // Pattern validation
    if (rules.pattern && typeof sanitizedValue === 'string') {
      if (!rules.pattern.test(sanitizedValue)) {
        errors.push('Invalid format')
      }
    }
    
    // Custom validation
    if (rules.custom) {
      const customResult = rules.custom(sanitizedValue)
      if (typeof customResult === 'string') {
        errors.push(customResult)
      } else if (!customResult) {
        errors.push('Invalid value')
      }
    }
    
    // Security checks
    const securityValidation = this.validateSecurity(sanitizedValue)
    if (!securityValidation.isValid) {
      errors.push(...securityValidation.errors)
    }
    
    return {
      isValid: errors.length === 0,
      errors,
      sanitizedValue
    }
  }
  
  /**
   * Validate multiple fields
   */
  public static validateFields(
    data: Record<string, any>,
    rules: Record<string, ValidationRule>
  ): { isValid: boolean; errors: Record<string, string[]>; sanitizedData: Record<string, any> } {
    const errors: Record<string, string[]> = {}
    const sanitizedData: Record<string, any> = {}
    
    Object.entries(rules).forEach(([field, rule]) => {
      const result = this.validate(data[field], rule)
      
      if (!result.isValid) {
        errors[field] = result.errors
      }
      
      sanitizedData[field] = result.sanitizedValue
    })
    
    return {
      isValid: Object.keys(errors).length === 0,
      errors,
      sanitizedData
    }
  }
  
  private static isEmpty(value: any): boolean {
    return value === null || 
           value === undefined || 
           value === '' || 
           (Array.isArray(value) && value.length === 0)
  }
  
  private static validateType(value: any, type: string): ValidationResult {
    const errors: string[] = []
    
    switch (type) {
      case 'string':
        if (typeof value !== 'string') {
          errors.push('Must be a string')
        }
        break
        
      case 'number':
        if (typeof value !== 'number' || isNaN(value)) {
          errors.push('Must be a valid number')
        }
        break
        
      case 'email':
        if (typeof value === 'string' && !validator.isEmail(value)) {
          errors.push('Must be a valid email address')
        }
        break
        
      case 'url':
        if (typeof value === 'string' && !validator.isURL(value)) {
          errors.push('Must be a valid URL')
        }
        break
        
      case 'phone':
        if (typeof value === 'string' && !validator.isMobilePhone(value)) {
          errors.push('Must be a valid phone number')
        }
        break
        
      case 'date':
        if (!validator.isISO8601(String(value))) {
          errors.push('Must be a valid date')
        }
        break
        
      case 'boolean':
        if (typeof value !== 'boolean') {
          errors.push('Must be a boolean value')
        }
        break
    }
    
    return {
      isValid: errors.length === 0,
      errors
    }
  }
  
  private static sanitizeInput(value: any, rules: ValidationRule): any {
    if (typeof value !== 'string') {
      return value
    }
    
    // Basic HTML sanitization
    let sanitized = DOMPurify.sanitize(value, {
      ALLOWED_TAGS: rules.allowedTags || [],
      ALLOWED_ATTR: rules.allowedAttributes ? 
        Object.keys(rules.allowedAttributes) : [],
      KEEP_CONTENT: true
    })
    
    // Additional sanitization
    sanitized = validator.escape(sanitized)
    sanitized = validator.trim(sanitized)
    
    return sanitized
  }
  
  private static validateSecurity(value: any): ValidationResult {
    const errors: string[] = []
    
    if (typeof value === 'string') {
      // Check for XSS patterns
      for (const pattern of this.XSS_PATTERNS) {
        if (pattern.test(value)) {
          errors.push('Potentially malicious content detected')
          break
        }
      }
      
      // Check for SQL injection patterns
      for (const pattern of this.SQL_INJECTION_PATTERNS) {
        if (pattern.test(value)) {
          errors.push('Potentially malicious SQL content detected')
          break
        }
      }
      
      // Check for path traversal
      if (value.includes('../') || value.includes('..\\')) {
        errors.push('Path traversal attempt detected')
      }
      
      // Check for null bytes
      if (value.includes('\0')) {
        errors.push('Null byte detected')
      }
    }
    
    return {
      isValid: errors.length === 0,
      errors
    }
  }
}

// Element Plus form validation integration
export const createSecureRules = (rules: Record<string, ValidationRule>) => {
  const elementRules: Record<string, any[]> = {}
  
  Object.entries(rules).forEach(([field, rule]) => {
    elementRules[field] = [
      {
        validator: (rule: any, value: any, callback: Function) => {
          const result = SecureValidator.validate(value, rules[field])
          
          if (result.isValid) {
            callback()
          } else {
            callback(new Error(result.errors[0]))
          }
        },
        trigger: 'blur'
      }
    ]
    
    if (rule.required) {
      elementRules[field].unshift({
        required: true,
        message: 'This field is required',
        trigger: 'blur'
      })
    }
  })
  
  return elementRules
}

Content Security Policy (CSP)

CSP Configuration

typescript
// src/utils/security/csp.ts
export interface CSPConfig {
  'default-src'?: string[]
  'script-src'?: string[]
  'style-src'?: string[]
  'img-src'?: string[]
  'font-src'?: string[]
  'connect-src'?: string[]
  'media-src'?: string[]
  'object-src'?: string[]
  'child-src'?: string[]
  'frame-src'?: string[]
  'worker-src'?: string[]
  'manifest-src'?: string[]
  'base-uri'?: string[]
  'form-action'?: string[]
  'frame-ancestors'?: string[]
  'report-uri'?: string
  'report-to'?: string
  'upgrade-insecure-requests'?: boolean
  'block-all-mixed-content'?: boolean
}

export class CSPManager {
  private static config: CSPConfig = {
    'default-src': ["'self'"],
    'script-src': [
      "'self'",
      "'unsafe-inline'", // Only for development
      "'unsafe-eval'", // Only for development
      'https://cdn.jsdelivr.net',
      'https://unpkg.com'
    ],
    'style-src': [
      "'self'",
      "'unsafe-inline'",
      'https://fonts.googleapis.com',
      'https://cdn.jsdelivr.net'
    ],
    'img-src': [
      "'self'",
      'data:',
      'https:',
      'blob:'
    ],
    'font-src': [
      "'self'",
      'https://fonts.gstatic.com',
      'https://cdn.jsdelivr.net'
    ],
    'connect-src': [
      "'self'",
      'https://api.example.com',
      'wss://ws.example.com'
    ],
    'media-src': ["'self'"],
    'object-src': ["'none'"],
    'child-src': ["'self'"],
    'frame-src': ["'self'"],
    'worker-src': ["'self'", 'blob:'],
    'manifest-src': ["'self'"],
    'base-uri': ["'self'"],
    'form-action': ["'self'"],
    'frame-ancestors': ["'none'"],
    'upgrade-insecure-requests': true,
    'block-all-mixed-content': true
  }
  
  /**
   * Generate CSP header value
   */
  public static generateCSPHeader(): string {
    const directives: string[] = []
    
    Object.entries(this.config).forEach(([directive, value]) => {
      if (typeof value === 'boolean') {
        if (value) {
          directives.push(directive)
        }
      } else if (typeof value === 'string') {
        directives.push(`${directive} ${value}`)
      } else if (Array.isArray(value)) {
        directives.push(`${directive} ${value.join(' ')}`)
      }
    })
    
    return directives.join('; ')
  }
  
  /**
   * Apply CSP to current page
   */
  public static applyCSP(): void {
    const meta = document.createElement('meta')
    meta.httpEquiv = 'Content-Security-Policy'
    meta.content = this.generateCSPHeader()
    document.head.appendChild(meta)
  }
  
  /**
   * Update CSP configuration
   */
  public static updateConfig(newConfig: Partial<CSPConfig>): void {
    this.config = { ...this.config, ...newConfig }
  }
  
  /**
   * Add source to directive
   */
  public static addSource(directive: keyof CSPConfig, source: string): void {
    const current = this.config[directive]
    if (Array.isArray(current)) {
      if (!current.includes(source)) {
        current.push(source)
      }
    }
  }
  
  /**
   * Remove source from directive
   */
  public static removeSource(directive: keyof CSPConfig, source: string): void {
    const current = this.config[directive]
    if (Array.isArray(current)) {
      const index = current.indexOf(source)
      if (index > -1) {
        current.splice(index, 1)
      }
    }
  }
  
  /**
   * Generate nonce for inline scripts/styles
   */
  public static generateNonce(): string {
    const array = new Uint8Array(16)
    crypto.getRandomValues(array)
    return btoa(String.fromCharCode(...array))
  }
  
  /**
   * Add nonce to CSP
   */
  public static addNonce(directive: 'script-src' | 'style-src', nonce: string): void {
    this.addSource(directive, `'nonce-${nonce}'`)
  }
}

// CSP violation reporting
export class CSPReporter {
  private static endpoint = '/api/security/csp-violations'
  
  /**
   * Initialize CSP violation reporting
   */
  public static initialize(): void {
    document.addEventListener('securitypolicyviolation', this.handleViolation)
  }
  
  private static handleViolation = (event: SecurityPolicyViolationEvent): void => {
    const violation = {
      documentURI: event.documentURI,
      referrer: event.referrer,
      blockedURI: event.blockedURI,
      violatedDirective: event.violatedDirective,
      effectiveDirective: event.effectiveDirective,
      originalPolicy: event.originalPolicy,
      sourceFile: event.sourceFile,
      lineNumber: event.lineNumber,
      columnNumber: event.columnNumber,
      statusCode: event.statusCode,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent
    }
    
    // Send violation report
    this.sendViolationReport(violation)
  }
  
  private static async sendViolationReport(violation: any): Promise<void> {
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(violation)
      })
    } catch (error) {
      console.error('Failed to send CSP violation report:', error)
    }
  }
}

Secure HTTP Client

typescript
// src/utils/http/secure-client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { TokenManager } from '../auth/token-manager'
import { RateLimiter } from './rate-limiter'
import { RequestSigner } from './request-signer'

export interface SecureClientConfig {
  baseURL: string
  timeout?: number
  retries?: number
  rateLimitConfig?: {
    maxRequests: number
    windowMs: number
  }
  enableRequestSigning?: boolean
  enableCSRFProtection?: boolean
  trustedDomains?: string[]
}

export class SecureHTTPClient {
  private instance: AxiosInstance
  private rateLimiter: RateLimiter
  private requestSigner?: RequestSigner
  private config: SecureClientConfig
  
  constructor(config: SecureClientConfig) {
    this.config = config
    
    // Initialize rate limiter
    this.rateLimiter = new RateLimiter(
      config.rateLimitConfig?.maxRequests || 100,
      config.rateLimitConfig?.windowMs || 60000
    )
    
    // Initialize request signer
    if (config.enableRequestSigning) {
      this.requestSigner = new RequestSigner()
    }
    
    // Create axios instance
    this.instance = axios.create({
      baseURL: config.baseURL,
      timeout: config.timeout || 10000,
      withCredentials: true,
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      }
    })
    
    this.setupInterceptors()
  }
  
  private setupInterceptors(): void {
    // Request interceptor
    this.instance.interceptors.request.use(
      async (config) => {
        // Rate limiting
        if (!this.rateLimiter.allowRequest()) {
          throw new Error('Rate limit exceeded')
        }
        
        // Add authentication token
        const token = TokenManager.getAccessToken()
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        
        // Add CSRF token
        if (this.config.enableCSRFProtection) {
          const csrfToken = this.getCSRFToken()
          if (csrfToken) {
            config.headers['X-CSRF-Token'] = csrfToken
          }
        }
        
        // Add request ID for tracking
        config.headers['X-Request-ID'] = this.generateRequestId()
        
        // Add timestamp
        config.headers['X-Timestamp'] = Date.now().toString()
        
        // Sign request if enabled
        if (this.requestSigner) {
          const signature = await this.requestSigner.signRequest(config)
          config.headers['X-Signature'] = signature
        }
        
        // Validate URL against trusted domains
        if (!this.isTrustedDomain(config.url || '')) {
          throw new Error('Untrusted domain')
        }
        
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // Response interceptor
    this.instance.interceptors.response.use(
      (response) => {
        // Validate response headers
        this.validateResponseHeaders(response)
        
        // Log successful requests
        this.logRequest(response.config, response.status)
        
        return response
      },
      async (error) => {
        // Handle token refresh
        if (error.response?.status === 401) {
          try {
            await TokenManager.refreshTokenIfNeeded()
            // Retry the original request
            return this.instance.request(error.config)
          } catch (refreshError) {
            // Redirect to login
            window.location.href = '/login'
            return Promise.reject(refreshError)
          }
        }
        
        // Log failed requests
        this.logRequest(error.config, error.response?.status || 0, error.message)
        
        return Promise.reject(error)
      }
    )
  }
  
  private validateResponseHeaders(response: AxiosResponse): void {
    const requiredHeaders = [
      'x-content-type-options',
      'x-frame-options',
      'x-xss-protection'
    ]
    
    requiredHeaders.forEach(header => {
      if (!response.headers[header]) {
        console.warn(`Missing security header: ${header}`)
      }
    })
  }
  
  private isTrustedDomain(url: string): boolean {
    if (!this.config.trustedDomains) {
      return true // Allow all if no trusted domains specified
    }
    
    try {
      const urlObj = new URL(url, this.config.baseURL)
      return this.config.trustedDomains.some(domain => 
        urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
      )
    } catch {
      return false
    }
  }
  
  private getCSRFToken(): string | null {
    // Get CSRF token from meta tag or cookie
    const metaToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
    if (metaToken) return metaToken
    
    // Fallback to cookie
    const cookies = document.cookie.split(';')
    const csrfCookie = cookies.find(cookie => cookie.trim().startsWith('XSRF-TOKEN='))
    return csrfCookie ? decodeURIComponent(csrfCookie.split('=')[1]) : null
  }
  
  private generateRequestId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  }
  
  private logRequest(config: any, status: number, error?: string): void {
    const logData = {
      method: config.method?.toUpperCase(),
      url: config.url,
      status,
      timestamp: new Date().toISOString(),
      requestId: config.headers?.['X-Request-ID'],
      error
    }
    
    if (error) {
      console.error('HTTP Request Failed:', logData)
    } else {
      console.log('HTTP Request:', logData)
    }
  }
  
  // Public API methods
  public async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.instance.get<T>(url, config)
    return response.data
  }
  
  public async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.instance.post<T>(url, data, config)
    return response.data
  }
  
  public async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.instance.put<T>(url, data, config)
    return response.data
  }
  
  public async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.instance.delete<T>(url, config)
    return response.data
  }
  
  public async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.instance.patch<T>(url, data, config)
    return response.data
  }
}

// Rate limiter implementation
class RateLimiter {
  private requests: number[] = []
  
  constructor(
    private maxRequests: number,
    private windowMs: number
  ) {}
  
  allowRequest(): boolean {
    const now = Date.now()
    
    // Remove old requests outside the window
    this.requests = this.requests.filter(time => now - time < this.windowMs)
    
    // Check if we can make a new request
    if (this.requests.length < this.maxRequests) {
      this.requests.push(now)
      return true
    }
    
    return false
  }
}

// Request signer implementation
class RequestSigner {
  private async signRequest(config: any): Promise<string> {
    const payload = {
      method: config.method,
      url: config.url,
      timestamp: config.headers['X-Timestamp'],
      data: config.data ? JSON.stringify(config.data) : ''
    }
    
    const encoder = new TextEncoder()
    const data = encoder.encode(JSON.stringify(payload))
    
    // In a real implementation, use a proper signing key
    const key = await crypto.subtle.importKey(
      'raw',
      encoder.encode('your-signing-key'),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']
    )
    
    const signature = await crypto.subtle.sign('HMAC', key, data)
    return btoa(String.fromCharCode(...new Uint8Array(signature)))
  }
}

This comprehensive security guide provides robust protection mechanisms for Element Plus applications, covering authentication, authorization, input validation, CSP, and secure HTTP communications.

Element Plus Study Guide