前端ElInput输入框全局拦截危险字符

96 阅读4分钟

vue3 + element-plus

1.原理

使用h函数重写el-input组件,注册到vue组件中

2.代码

inputInterceptor.ts

// src/directive/inputInterceptor.ts
// 输入拦截器 - 用于防止XSS攻击和恶意代码注入的安全组件

// 导入Vue应用类型,用于类型检查
import type { App } from 'vue'
// 导入Element Plus的消息组件,用于显示警告信息
import { ElMessage } from 'element-plus'
// 导入Vue的组合式API函数
import { defineComponent, h, ref, watch } from 'vue'

// 定义安全输入模式数组 - 包含所有需要拦截的危险字符和模式
// 预编译正则表达式以提高性能
const SAFE_INPUT_PATTERNS = [
  // JavaScript危险函数 - 可能被用于执行恶意代码
  { pattern: /alert\s*\(/gi, name: 'alert', level: 'high' }, // 弹窗函数
  { pattern: /eval\s*\(/gi, name: 'eval', level: 'high' }, // 动态执行代码函数
  { pattern: /confirm\s*\(/gi, name: 'confirm', level: 'high' }, // 确认对话框函数
  { pattern: /prompt\s*\(/gi, name: 'prompt', level: 'high' }, // 输入对话框函数
  { pattern: /setTimeout\s*\(/gi, name: 'setTimeout', level: 'high' }, // 定时器函数
  { pattern: /setInterval\s*\(/gi, name: 'setInterval', level: 'high' }, // 定时器函数
  { pattern: /Function\s*\(/gi, name: 'function', level: 'high' }, // 函数构造函数

  // HTML危险标签 - 可能被用于注入恶意HTML
  { pattern: /<script\b[^>]*>/gi, name: '<script>', level: 'high' }, // 脚本标签
  { pattern: /<\/script>/gi, name: '</script>', level: 'high' }, // 脚本标签结束
  { pattern: /<svg\b[^>]*>/gi, name: '<svg>', level: 'medium' }, // SVG标签
  { pattern: /<\/svg>/gi, name: '</svg>', level: 'medium' }, // SVG标签结束
  { pattern: /<img\b[^>]*>/gi, name: '<img>', level: 'medium' }, // 图片标签
  { pattern: /<iframe\b[^>]*>/gi, name: '<iframe>', level: 'high' }, // iframe标签
  { pattern: /<object\b[^>]*>/gi, name: '<object>', level: 'high' }, // object标签
  { pattern: /<embed\b[^>]*>/gi, name: '<embed>', level: 'high' }, // embed标签

  // 事件处理器属性 - 可能被用于执行恶意事件
  { pattern: /\bon\w+\s*=/gi, name: 'on', level: 'high' }, // 所有on事件
  { pattern: /\bonerror\s*=/gi, name: 'onerror', level: 'high' }, // 错误事件
  { pattern: /\bonload\s*=/gi, name: 'onload', level: 'high' }, // 加载事件
  { pattern: /\boninput\s*=/gi, name: 'oninput', level: 'medium' }, // 输入事件
  { pattern: /\bonclick\s*=/gi, name: 'onclick', level: 'medium' }, // 点击事件
  { pattern: /\bonmouse\w+\s*=/gi, name: 'mouse', level: 'medium' }, // 鼠标相关事件

  // 属性选择器 - 可能被用于注入恶意属性
  { pattern: /\bsrc\s*=/gi, name: 'src', level: 'medium' }, // 源属性
  { pattern: /\bhref\s*=/gi, name: 'href', level: 'medium' }, // 链接属性
  { pattern: /\bdata\s*=/gi, name: 'data', level: 'low' }, // data属性

  // URL编码字符 - 可能被用于绕过检测
  { pattern: /%3[cC]/gi, name: '<', level: 'medium' }, // URL编码的 '<'
  { pattern: /%3[eE]/gi, name: '>', level: 'medium' }, // URL编码的 '>'
  { pattern: /%2[bB]/gi, name: '+', level: 'low' }, // URL编码的 '+'
  { pattern: /%2[fF]/gi, name: '/', level: 'low' }, // URL编码的 '/'
  // { pattern: /%2[27]/gi, name: '"', level: 'medium' }, // URL编码的 '"'
  // { pattern: /%2[79]/gi, name: "'", level: 'medium' }, // URL编码的 "'"

  // JavaScript协议 - 可能被用于执行恶意代码
  { pattern: /javascript\s*:/gi, name: 'javascript:', level: 'high' }, // JavaScript协议
  { pattern: /vbscript\s*:/gi, name: 'vbscript:', level: 'high' }, // VBScript协议
  { pattern: /data\s*:\s*text\/html/gi, name: 'data:text/html', level: 'high' }, // data协议

  // 特殊字符组合 - 可能被用于构造恶意代码
  { pattern: /<[^>]*>/gi, name: 'HTML', level: 'medium' }, // 任何HTML标签
  { pattern: /\\x[0-9a-fA-F]{2}/gi, name: 'Ox', level: 'medium' }, // 十六进制编码
  { pattern: /\\u[0-9a-fA-F]{4}/gi, name: 'unicode', level: 'medium' }, // Unicode编码

  // 特殊字符 - 可能被用于构造恶意代码或绕过检测
  { pattern: /@/gi, name: '@', level: 'low' }, // @符号,常用于邮箱地址
  { pattern: /!/gi, name: '!', level: 'low' }, // 感叹号,可能用于构造特殊语法
]

// 配置选项接口
interface InterceptorConfig {
  enableWarning?: boolean // 是否启用警告提示
  enableLogging?: boolean // 是否启用日志记录
  strictMode?: boolean // 严格模式,检测更多模式
  whitelist?: string[] // 白名单,允许特定模式
  maxLength?: number // 最大输入长度
  customPatterns?: Array<{
    // 自定义检测模式
    pattern: RegExp
    name: string
    level: 'high' | 'medium' | 'low'
  }>
  customValidator?: (value: string) => {
    // 自定义校验函数
    isValid: boolean
    message?: string
    cleanedValue?: string
  }
}

// 默认配置
const DEFAULT_CONFIG: InterceptorConfig = {
  enableWarning: true,
  enableLogging: false,
  strictMode: false,
  whitelist: [],
  maxLength: 1000,
}

/**
 * 安全检测函数
 * @param value 要检测的值
 * @param config 配置选项
 * @returns 检测结果
 */
function securityCheck(
  value: string,
  config: InterceptorConfig = DEFAULT_CONFIG
) {
  if (!value || typeof value !== 'string') {
    return { isSafe: true, cleanedValue: value, detectedPatterns: [] }
  }

  // 检查长度限制
  if (config.maxLength && value.length > config.maxLength) {
    return {
      isSafe: false,
      cleanedValue: value.substring(0, config.maxLength),
      detectedPatterns: [`输入长度超过限制(${config.maxLength}字符)`],
      level: 'medium',
    }
  }

  let cleanedValue = value
  let detectedPatterns: Array<{ name: string; level: string }> = []

  // 遍历所有危险模式进行检测
  SAFE_INPUT_PATTERNS.forEach(({ pattern, name, level }) => {
    // 检查是否在白名单中
    if (config.whitelist && config.whitelist.includes(name)) {
      return
    }

    // 使用正则表达式测试当前值是否包含危险模式
    if (pattern.test(value)) {
      detectedPatterns.push({ name, level })
      // 从值中移除检测到的危险字符
      cleanedValue = cleanedValue.replace(pattern, '')
    }
  })

  // 检测自定义模式
  if (config.customPatterns) {
    config.customPatterns.forEach(({ pattern, name, level }) => {
      if (pattern.test(value)) {
        detectedPatterns.push({ name, level })
        cleanedValue = cleanedValue.replace(pattern, '')
      }
    })
  }

  // 在严格模式下进行额外检查
  if (config.strictMode) {
    // 检查SQL注入模式
    const sqlPatterns = [
      {
        pattern: /(\b(select|insert|update|delete|drop|create|alter)\b)/gi,
        name: 'SQL关键字',
        level: 'medium',
      },
      { pattern: /(--|\/\*|\*\/)/gi, name: 'SQL注释', level: 'medium' },
      {
        pattern: /(\b(union|exec|execute)\b)/gi,
        name: 'SQL命令',
        level: 'high',
      },
    ]

    sqlPatterns.forEach(({ pattern, name, level }) => {
      if (pattern.test(value)) {
        detectedPatterns.push({ name, level })
        cleanedValue = cleanedValue.replace(pattern, '')
      }
    })
  }

  // 执行自定义校验函数
  if (config.customValidator) {
    try {
      const customResult = config.customValidator(cleanedValue)
      if (!customResult.isValid) {
        detectedPatterns.push({
          name: customResult.message || '自定义校验失败',
          level: 'medium',
        })
        if (customResult.cleanedValue !== undefined) {
          cleanedValue = customResult.cleanedValue
        }
      }
    } catch (error) {
      console.warn('自定义校验函数执行失败:', error)
    }
  }

  return {
    isSafe: detectedPatterns.length === 0,
    cleanedValue,
    detectedPatterns,
    level: detectedPatterns.length > 0 ? detectedPatterns[0].level : 'safe',
  }
}

/**
 * 设置输入拦截器的主函数
 * @param app Vue应用实例
 * @param config 配置选项
 */
export function setupInputInterceptor(
  app: App,
  config: InterceptorConfig = DEFAULT_CONFIG
) {
  // 合并配置
  const finalConfig = { ...DEFAULT_CONFIG, ...config }

  // 获取应用中已注册的原始ElInput组件
  const originalInput = app.component('ElInput')

  // 如果原始组件不存在,直接返回,不进行拦截
  if (!originalInput) {
    console.warn('ElInput组件未找到,输入拦截器未启用')
    return
  }

  // 定义拦截后的输入组件,继承原始组件的所有功能
  const InterceptedInput = defineComponent({
    name: 'ElInput', // 保持原始组件名称
    inheritAttrs: false, // 不自动继承属性,手动处理

    // 定义组件属性,与原始ElInput保持一致
    props: {
      modelValue: {
        type: [String, Number], // 支持字符串和数字类型
        default: '', // 默认值为空字符串
      },
      placeholder: {
        type: String,
        default: '', // 占位符文本
      },
      disabled: {
        type: Boolean,
        default: false, // 是否禁用
      },
      readonly: {
        type: Boolean,
        default: false, // 是否只读
      },
      clearable: {
        type: Boolean,
        default: false, // 是否可清空
      },
      showPassword: {
        type: Boolean,
        default: false, // 是否显示密码切换按钮
      },
      type: {
        type: String,
        default: 'text', // 输入框类型
      },
      size: {
        type: String,
        default: 'default', // 组件大小
      },
      // 新增安全配置属性
      securityConfig: {
        type: Object as () => InterceptorConfig,
        default: () => ({}),
      },
    },

    // 定义组件可以触发的事件
    emits: [
      'update:modelValue',
      'input',
      'change',
      'focus',
      'blur',
      'clear',
      'security-violation',
    ],

    // 组件的设置函数,使用组合式API
    setup(props, { emit, attrs }) {
      // 响应式变量用于跟踪安全违规次数
      const violationCount = ref(0)
      const lastViolationTime = ref(0)

      // 合并组件级别的安全配置
      const getComponentConfig = () => {
        return { ...finalConfig, ...props.securityConfig }
      }

      /**
       * 处理模型值更新的核心函数
       * @param value 用户输入的新值
       */
      const handleUpdateModelValue = (value: string) => {
        const componentConfig = getComponentConfig()

        // 进行安全检查
        const checkResult = securityCheck(value, componentConfig)

        // 如果检测到安全问题
        if (!checkResult.isSafe) {
          // 更新违规计数
          violationCount.value++
          lastViolationTime.value = Date.now()

          // 更新模型值为清理后的安全值
          emit('update:modelValue', checkResult.cleanedValue)
          emit('input', checkResult.cleanedValue)

          // 触发安全违规事件
          emit('security-violation', {
            originalValue: value,
            cleanedValue: checkResult.cleanedValue,
            detectedPatterns: checkResult.detectedPatterns,
            violationCount: violationCount.value,
            timestamp: Date.now(),
          })

          // 显示警告消息
          if (componentConfig.enableWarning) {
            const patternNames = checkResult.detectedPatterns
              .map((p) => p.name)
              .join('、')
            const level = checkResult.level === 'high' ? 'error' : 'warning'

            ElMessage({
              type: level,
              message: `检测到${
                checkResult.level === 'high' ? '高危' : '中危'
              }安全威胁: ${patternNames},已自动清除`,
              duration: 3000,
            })
          }

          // 记录日志
          if (componentConfig.enableLogging) {
            console.warn('安全拦截:', {
              originalValue: value,
              cleanedValue: checkResult.cleanedValue,
              detectedPatterns: checkResult.detectedPatterns,
              timestamp: new Date().toISOString(),
            })
          }

          return // 提前返回,不执行后续的正常更新逻辑
        }

        // 如果没有检测到危险字符,正常更新值
        emit('update:modelValue', value)
        emit('input', value)
      }

      // 监听props变化,重新检查当前值
      watch(
        () => props.modelValue,
        (newValue) => {
          if (newValue && typeof newValue === 'string') {
            const componentConfig = getComponentConfig()
            const checkResult = securityCheck(newValue, componentConfig)

            if (!checkResult.isSafe) {
              // 如果当前值不安全,自动清理
              emit('update:modelValue', checkResult.cleanedValue)
            }
          }
        }
      )

      // 返回渲染函数
      return () => {
        // 使用Vue的h函数创建虚拟DOM
        return h(originalInput, {
          ...attrs, // 展开所有传入的属性
          ...props, // 展开所有props
          'onUpdate:modelValue': handleUpdateModelValue, // 重写模型值更新处理器
          onInput: attrs.onInput, // 透传原始的input事件处理器
        })
      }
    },
  })

  // 将拦截后的组件重新注册为ElInput,覆盖原始组件
  app.component('ElInput', InterceptedInput)

  // 记录拦截器启用日志
  if (finalConfig.enableLogging) {
    console.log('输入拦截器已启用,配置:', finalConfig)
  }
}

// 导出配置类型和默认配置,供外部使用
export type { InterceptorConfig }
export { DEFAULT_CONFIG, securityCheck }

3.使用

注册

main.js

import {createApp} from 'vue';
import App from '/@/App.vue';
import {setupInputInterceptor} from '/@components/interceptor/inputInterceptor';
import ElementPlus from 'element-plus';

const app = createApp(App)
// 必须在Element-Plus之后注册
app.use(pinia).use(router).use(ElementPlus);
setupInputInterceptor(app);
app.mount('#app')

4.示例

执行顺序总结:
安全拦截器检测 → 清理危险字符
自定义模式检测 → 清理自定义模式
自定义校验函数 → 执行业务逻辑校验
触发input事件 → 执行你的@input方法

(1)### 并存模式

<template>
  <el-input 
    v-model="email"
    @input="handleEmailValidation"
    :security-config="{ 
      enableWarning: true,
      whitelist: ['@'],
      customValidator: validateEmailFormat
    }"
  />
</template>

<script setup>
const email = ref('')

// 在安全拦截器内部执行的校验(优先)
const validateEmailFormat = (value) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  
  if (value && !emailRegex.test(value)) {
    return {
      isValid: false,
      message: '邮箱格式不正确',
      cleanedValue: ''
    }
  }
  
  return { isValid: true }
}

// 在@input中执行的额外逻辑(后执行)
const handleEmailValidation = (value) => {
  // 可以执行一些副作用操作
  console.log('邮箱输入:', value)
  
  // 或者执行一些不需要阻止输入的校验
  if (value && value.length > 50) {
    ElMessage.warning('邮箱地址过长')
  }
}
</script>

(1)只使用安全拦截器

<template>
  <el-input 
    v-model="email"
    :security-config="{ 
      enableWarning: true,
      whitelist: ['@'] 
    }"
    <!-- 不添加 @input 事件 -->
  />
</template>

(2)只使用自定义校验

<template>
  <el-input 
    v-model="email"
    @input="handleEmailValidation"
    :security-config="{ 
      enableWarning: false,  <!-- 关闭安全拦截器 -->
      whitelist: ['@'] 
    }"
  />
</template>

(3)使用自定义校验函数替代@input

<template>
  <el-input 
    v-model="email"
    :security-config="{ 
      enableWarning: true,
      whitelist: ['@'],
      customValidator: validateEmail  <!-- 在安全拦截器内部执行 -->
    }"
  />
</template>

<script setup>
const validateEmail = (value) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  
  if (value && !emailRegex.test(value)) {
    return {
      isValid: false,
      message: '请输入有效的邮箱地址',
      cleanedValue: '' // 清空无效输入
    }
  }
  
  return { isValid: true }
}
</script>

(4)条件性执行

<template>
  <el-input 
    v-model="email"
    @input="shouldValidate ? handleEmailValidation : undefined"
    :security-config="{ 
      enableWarning: true,
      whitelist: ['@'] 
    }"
  />
</template>

<script setup>
const shouldValidate = ref(true)

const handleEmailValidation = (value) => {
  // 只在需要时执行校验
  if (shouldValidate.value) {
    // 校验逻辑
  }
}
</script>