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