企业 AI Coding 治理:前端侧怎么拦住代码泄露、Prompt 注入和敏感数据外泄

2 阅读1分钟

企业 AI Coding 治理:前端侧怎么拦住代码泄露、Prompt 注入和敏感数据外泄

去年底公司全面接入 AI Coding 助手,开发效率肉眼可见地提升了。但安全团队的脸色也肉眼可见地难看——某天晚上收到告警,有人在跟 AI 对话时把一段包含数据库连接串的配置文件原封不动贴了进去,连接串里带着生产环境的密码。从那之后,我花了将近三个月,在前端侧搭了一套 AI Coding 治理管控体系。代码泄露防护、Prompt 注入审计、敏感数据脱敏,三条线同时推。今天把这套方案完整拆开聊聊。

代码泄露防护:用指纹识别拦住核心资产外流

SimHash 指纹生成

我们选择了 SimHash 做代码指纹。和精确哈希(SHA-256)不同,SimHash 是一种局部敏感哈希——两段代码即使有少量修改(改了变量名、删了几行注释),SimHash 值仍然相近。这对代码泄露检测至关重要,因为开发者贴出去的代码几乎不会和仓库里的完全一致,可能删了几行、改了缩进、去掉了注释。指纹生成的流程:先对代码做标准化预处理(去除注释、统一空白符、统一变量名为占位符),然后按滑动窗口切分为 N-gram 特征,最后计算 SimHash 值。

class CodeFingerprintGenerator {
  // 预处理:去注释、统一空白、标准化标识符
  private normalize(code: string): string {
    return code
      .replace(/\/\/.*$/gm, '')           // 单行注释
      .replace(/\/\*[\s\S]*?\*\//g, '')   // 多行注释
      .replace(/['"][^'"]*['"]/g, '"STR"') // 字符串字面量统一
      .replace(/\s+/g, ' ')               // 空白统一
      .trim()
  }

  // 滑动窗口提取 N-gram 特征
  private extractNgrams(text: string, n: number = 4): string[] {
    const tokens = text.split(/\s+/)
    const ngrams: string[] = []
    for (let i = 0; i <= tokens.length - n; i++) {
      ngrams.push(tokens.slice(i, i + n).join(' '))
    }
    return ngrams
  }

  // 计算 SimHash(64 位)
  generateFingerprint(code: string): bigint {
    const normalized = this.normalize(code)
    const ngrams = this.extractNgrams(normalized)
    const bits = new Int32Array(64)

    for (const gram of ngrams) {
      const hash = this.fnv64(gram) // FNV-1a 64位哈希
      for (let i = 0; i < 64; i++) {
        bits[i] += (hash >> BigInt(i)) & 1n ? 1 : -1
      }
    }

    let fingerprint = 0n
    for (let i = 0; i < 64; i++) {
      if (bits[i] > 0) fingerprint |= 1n << BigInt(i)
    }
    return fingerprint
  }

  // 汉明距离:两个指纹有多少位不同
  hammingDistance(a: bigint, b: bigint): number {
    let xor = a ^ b
    let count = 0
    while (xor > 0n) {
      count += Number(xor & 1n)
      xor >>= 1n
    }
    return count
  }

  private fnv64(str: string): bigint {
    let hash = 0xcbf29ce484222325n
    for (let i = 0; i < str.length; i++) {
      hash ^= BigInt(str.charCodeAt(i))
      hash = BigInt.asUintN(64, hash * 0x100000001b3n)
    }
    return hash
  }
}

核心仓库代码库构建

指纹算法有了,接下来是"跟谁比"。我们不可能把公司所有代码都建指纹库——仓库太多、代码量太大,全量比对性能也扛不住。实际做法是安全团队和各业务线 Tech Lead 一起圈定核心资产仓库,主要包括三类:涉及支付/风控/加密的核心业务逻辑、自研中间件和基础框架、包含专利算法的模块。圈定后,CI 流水线在每次核心仓库合入主干时自动触发指纹更新。

// 前端侧的指纹匹配服务
class CodeLeakDetector {
  private buckets: Map<number, FingerprintEntry[]> = new Map()
  private generator = new CodeFingerprintGenerator()
  private readonly THRESHOLD = 6 // 汉明距离阈值

  // 加载指纹库并按前 8 位分桶,加速查询
  loadLibrary(entries: FingerprintEntry[]) {
    for (const entry of entries) {
      const bucketKey = Number(entry.fingerprint & 0xFFn)
      if (!this.buckets.has(bucketKey)) this.buckets.set(bucketKey, [])
      this.buckets.get(bucketKey)!.push(entry)
    }
  }

  // 检测用户输入是否命中核心仓库代码
  detect(input: string): LeakDetectionResult {
    // 短文本跳过(贴几行代码不构成泄露风险)
    if (input.split('\n').length < 15) {
      return { matched: false }
    }

    const inputFingerprint = this.generator.generateFingerprint(input)
    const bucketKey = Number(inputFingerprint & 0xFFn)
    const candidates = this.buckets.get(bucketKey) ?? []

    let bestMatch: FingerprintEntry | null = null
    let bestDistance = Infinity

    for (const entry of candidates) {
      const dist = this.generator.hammingDistance(
        inputFingerprint, entry.fingerprint
      )
      if (dist < bestDistance) {
        bestDistance = dist
        bestMatch = entry
      }
    }

    if (bestDistance <= this.THRESHOLD && bestMatch) {
      return {
        matched: true,
        repo: bestMatch.repo,
        filePath: bestMatch.filePath,
        distance: bestDistance,
        action: bestDistance <= 3 ? 'BLOCK' : 'REVIEW',
      }
    }
    return { matched: false }
  }
}

匹配策略与分级处置

汉明距离阈值设为 6(64 位指纹中允许 6 位不同),是反复调参的结果。低于 3 判定为高度相似,直接拦截并通知安全团队;3-6 之间判定为疑似泄露,触发审批流,开发者需要说明理由后由 Tech Lead 审批放行;大于 6 则放行。

实际运行中还有一个容易忽略的问题:开发者可能只贴了核心文件中的一个函数(20 行左右),而我们的指纹切片是 200 行窗口。对此我们补充了一个关键函数签名匹配作为辅助手段——安全团队可以手动标记某些函数签名(如 function calcRiskScoreclass PaymentEngine),前端做简单的字符串匹配作为 SimHash 的补充。这一层很轻,但实测额外拦住了约 12% 的短片段泄露。

敏感数据脱敏:最脏最累但最不能省的活

你以为的敏感数据 vs 实际的敏感数据

列一下我们实际拦截到的敏感数据类型,可能跟直觉不太一样:| 类型 | 你以为的占比 | 实际占比 | |------|------------|---------| | API Key / Secret | 大头 | 15% | | 数据库连接串 | 不少 | 8% | | 内部域名 / IP | 没想过 | 32% | | 员工姓名 / 工号 | 应该没人贴 | 18% | | 业务数据(订单号、手机号) | 少量 | 22% | | JWT Token / Session ID | 偶尔 | 5% |

内部域名和 IP 占比最高,这是完全没料到的。开发者贴代码的时候,配置文件、环境变量、甚至注释里的 // 部署到 10.8.x.x 都会带进去。

脱敏引擎的分层设计

脱敏不是简单的正则替换——你得处理各种变体、上下文相关的判断,还得保证替换后代码能被 AI 理解。我们的脱敏引擎分三层。第一层:确定性模式匹配,格式固定的东西用正则就够了。这一层速度最快,覆盖面最广,能处理掉约 60% 的敏感数据。

const deterministicRules: RedactRule[] = [
  {
    name: 'aws_access_key',
    pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
    replace: '<AWS_KEY_REDACTED>',
  },
  {
    name: 'jwt_token',
    // 三段式 base64,中间用 . 连接
    pattern: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
    replace: '<JWT_REDACTED>',
  },
  {
    name: 'phone_number',
    pattern: /\b(1[3-9]\d)\d{4}(\d{4})\b/g,
    replace: '$1****$2', // 保留前三后四
  },
  {
    name: 'internal_ip',
    pattern: /\b(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
    replace: '<INTERNAL_IP>',
  },
  {
    name: 'generic_secret',
    // 匹配常见的 key=value 模式中的密钥值
    pattern: /(?<=(api[_-]?key|secret[_-]?key|access[_-]?token)\s*[=:]\s*['"]?)([A-Za-z0-9+/=_-]{20,})(?=['"]?)/gi,
    replace: '<SECRET_REDACTED>',
  },
]

第二层:上下文感知检测,需要看前后文才能判断。比如 password= 不能一律拦截,代码示例里也会出现这个词。得看 host、port、password 是否同时出现——三要素里命中两个以上才判定为连接串,并且只脱敏 password 的值,保留其他部分让 AI 能理解上下文。

// 上下文感知检测示例:数据库连接串
const contextAwareRules: ContextRule[] = [
  {
    name: 'connection_string',
    detect: (text: string) => {
      const hasHost = /(?:host|server)\s*[=:]\s*\S+/i.test(text)
      const hasPassword = /(?:password|passwd|pwd)\s*[=:]\s*\S+/i.test(text)
      const hasPort = /(?:port)\s*[=:]\s*\d+/i.test(text)
      return [hasHost, hasPassword, hasPort].filter(Boolean).length >= 2
    },
    redact: (text: string) => {
      return text.replace(
        /((?:password|passwd|pwd)\s*[=:]\s*)(\S+)/gi,
        '$1<REDACTED>'
      )
    },
  },
  {
    name: 'employee_info_in_context',
    // 工号+姓名同时出现才脱敏,避免误伤普通人名
    detect: (text: string) => {
      const hasWorkId = /工号\s*[::]\s*[A-Z]?\d{5,8}/i.test(text)
      const hasName = /(?:姓名|负责人|联系人)\s*[::]\s*[\u4e00-\u9fa5]{2,4}/.test(text)
      return hasWorkId || hasName
    },
    redact: (text: string) => {
      return text
        .replace(/(工号\s*[::]\s*)([A-Z]?\d{5,8})/gi, '$1<EMPLOYEE_ID>')
        .replace(/((?:姓名|负责人|联系人)\s*[::]\s*)([\u4e00-\u9fa5]{2,4})/g, '$1<NAME_REDACTED>')
    },
  },
]

第三层:动态企业规则,从后端动态拉取,支持热更新。

class DynamicRuleEngine {
  private rules: RedactRule[] = []
  private lastSync = 0

  async sync() {
    const resp = await fetch('/api/security/redact-rules', {
      headers: { 'If-Modified-Since': new Date(this.lastSync).toUTCString() },
    })
    if (resp.status === 304) return // 无更新
    const { rules, version } = await resp.json()
    // 后端下发的规则是序列化格式,需要编译为 RegExp
    this.rules = rules.map((r: RawRule) => ({
      name: r.name,
      pattern: new RegExp(r.pattern, r.flags),
      replace: r.replace,
      priority: r.priority ?? 0,
    }))
    this.lastSync = Date.now()
    console.log(`[RedactEngine] 动态规则已更新至 v${version},共 ${this.rules.length} 条`)
  }

  apply(text: string): string {
    // 按优先级排序执行,高优先级先执行
    const sorted = [...this.rules].sort((a, b) => b.priority - a.priority)
    let result = text
    for (const rule of sorted) {
      result = result.replace(rule.pattern, rule.replace)
    }
    return result
  }
}

// 三层引擎的串联调用
class RedactEngine {
  constructor(
    private deterministic: RedactRule[],
    private contextAware: ContextRule[],
    private dynamic: DynamicRuleEngine
  ) {}

  process(text: string): { result: string; hits: string[] } {
    const hits: string[] = []
    let result = text

    // 第一层:确定性匹配
    for (const rule of this.deterministic) {
      if (rule.pattern.test(result)) {
        hits.push(rule.name)
        result = result.replace(rule.pattern, rule.replace)
      }
    }
    // 第二层:上下文感知
    for (const rule of this.contextAware) {
      if (rule.detect(result)) {
        hits.push(rule.name)
        result = rule.redact(result)
      }
    }
    // 第三层:动态企业规则
    const beforeDynamic = result
    result = this.dynamic.apply(result)
    if (result !== beforeDynamic) hits.push('dynamic_rule')

    return { result, hits }
  }
}

脱敏后内容要"能用"

这是很多脱敏方案忽略的关键问题。把 host: '10.8.1.5' 替换成 host: '' 或者干脆删掉,AI 会困惑——这是空字符串?还是忘了填?配置文件有语法错误?

我们的原则是:脱敏后的内容要保留语义结构。用 <INTERNAL_HOST> 替代 IP 地址,用 <REDACTED> 替代密码,端口号不算敏感就保留原值。AI 看到 <INTERNAL_HOST> 能理解这是一个被隐藏的内部地址,而不是一个 bug。

我们甚至给 AI 的系统 Prompt 加了一条说明:

用户输入中可能包含 <REDACTED><INTERNAL_IP> 等占位符,这些是安全脱敏标记。请基于上下文推断其用途,不要要求用户提供真实值。

整体架构:前端治理层怎么跟后端协同

三个模块拆开讲完了,来看它们怎么拼在一起。整体数据流如下图所示:

┌─────────────────────────────────────────────────────────────────┐
│  用户输入(发送 / 粘贴 / 上传文件)                                │
└──────────────────────────┬──────────────────────────────────────┘
                           ▼
               ┌───────────────────────┐
               │   粘贴拦截 Hook        │  ← 超大文本走 Web Worker
               │   (长度/格式预检)      │
               └───────────┬───────────┘
                           ▼
          ┌────────────────┴────────────────┐
          ▼                                 ▼
┌──────────────────┐              ┌──────────────────┐
│ Prompt 注入检测器 │              │ 代码指纹匹配      │
│ (规则+结构分析)   │              │ (SimHash 比对)    │
└────────┬─────────┘              └────────┬─────────┘
         └────────────────┬────────────────┘
                          ▼
              ┌───────────────────────┐
              │  敏感数据脱敏引擎      │
              │  (三层:确定性/上下文/  │
              │   动态企业规则)        │
              └───────────┬───────────┘
                          ▼
              ┌───────────────────────┐       ┌─────────────────┐
              │  请求拦截 Proxy        │──────▶│ 审计分析后台     │
              │  (最终放行/拦截决策)    │       │ (异步上报,安全   │
              └───────────┬───────────┘       │  团队使用)       │
                          ▼                   └─────────────────┘
                 ┌─────────────────┐
                 │  AI Coding 后端  │
                 └─────────────────┘

请求拦截 Proxy 是所有模块的串联出口,核心逻辑是汇总各环节的检测结果做最终决策:

class RequestInterceptProxy {
  constructor(
    private injectionDetector: PromptInjectionDetector,
    private leakDetector: CodeLeakDetector,
    private redactEngine: RedactEngine
  ) {}

  async intercept(request: AIRequest): Promise<AIRequest | BlockedResponse> {
    const { content } = request

    // 1. Prompt 注入检测
    const injectionResult = await this.injectionDetector.analyze(content)
    if (injectionResult.blocked) {
      this.reportAudit(request, 'injection_blocked', injectionResult)
      return { blocked: true, reason: '检测到 Prompt 注入风险,请修改后重试' }
    }

    // 2. 代码泄露检测
    const leakResult = this.leakDetector.detect(content)
    if (leakResult.matched && leakResult.action === 'BLOCK') {
      this.reportAudit(request, 'code_leak_blocked', leakResult)
      return {
        blocked: true,
        reason: `该内容与核心仓库 ${leakResult.repo} 高度相似,禁止发送`,
      }
    }
    if (leakResult.matched && leakResult.action === 'REVIEW') {
      this.reportAudit(request, 'code_leak_review', leakResult)
      return { blocked: true, reason: '该内容疑似核心代码,已提交审批流' }
    }

    // 3. 敏感数据脱敏(不拦截,自动处理)
    const { result: redactedContent, hits } = this.redactEngine.process(content)
    if (hits.length > 0) {
      this.reportAudit(request, 'redacted', { hits })
    }

    // 4. 放行,使用脱敏后的内容
    return { ...request, content: redactedContent }
  }

  private reportAudit(request: AIRequest, action: string, detail: unknown) {
    // 异步上报,不阻塞主流程
    navigator.sendBeacon('/api/audit', JSON.stringify({
      sessionId: request.sessionId,
      action,
      detail,
      timestamp: Date.now(),
    }))
  }
}

性能影响控制

往每次请求里插这么多检测逻辑,性能是个绕不开的问题。实测数据:| 检测环节 | 平均耗时 | P99 耗时 | |---------|---------|---------| | 正则模式匹配 | 2ms | 8ms | | SimHash 指纹匹配 | 5ms | 15ms | | Prompt 注入检测 | 3ms | 12ms | | 敏感数据脱敏 | 8ms | 25ms | | 总计 | 18ms | 60ms |

60ms 的 P99 在一次 AI 请求(通常要等几秒才有结果)里完全可以接受。但超大文本需要特殊处理——有人一次粘贴 5000 行代码的情况确实存在。我们对超过 2000 行的输入走 Web Worker 异步处理,避免阻塞主线程。Worker 用完即销毁,不占常驻内存。

三个月实践数据和经验提炼

这套方案从去年 Q4 开始灰度,到全量运行三个月,几个关键数据:

指标上线前上线后
月均敏感数据外发事件无法统计(无监控)拦截 340+ 次
其中自动脱敏放行-78%
其中直接拦截-15%
其中触发审批流-7%
Prompt 注入检测-月均 flagged 120+ 次
代码指纹命中-月均 45 次
误报率(初期 / 当前)-23% / 3.8%
开发者满意度-4.1/5(内部调研)

几个数据值得展开说。

4.1 分的满意度说明开发者基本接受了这套方案,但还有提升空间。

脱敏与回答质量的平衡:语义摘要方案

我们正在尝试一个折中方案:对脱敏内容生成一份"语义摘要"附在请求里,让 AI 在不知道真实值的情况下依然能理解架构上下文。下面是一个实际的 before/after 对比。

脱敏前(原始输入):

# database.yml
production:
  host: 10.8.1.5
  port: 5432
  database: order_db
  username: svc_order
  password: Kj8#mP2$xL9
  pool: 20

用户提问:这个数据库连接池经常耗尽,帮我分析可能的原因并给出优化配置。

纯脱敏(没有语义摘要):

# database.yml
production:
  host: <INTERNAL_IP>
  port: 5432
  database: <REDACTED>
  username: <REDACTED>
  password: <REDACTED>
  pool: 20

AI 回答(节选):"连接池大小设为 20,这个值是否合理取决于你的数据库类型和部署架构。建议你提供更多信息,比如使用的是什么数据库、是单实例还是集群……" —— AI 不知道这是 PostgreSQL,也不知道部署架构,给出的是泛泛而谈的通用建议。

脱敏 + 语义摘要:

# database.yml
production:
  host: <INTERNAL_IP>  # PostgreSQL 主库,内网单实例部署
  port: 5432
  database: <DB_NAME>  # 订单服务主库,日均写入约 50 万条
  username: <REDACTED>  # 服务账号,非 superuser
  password: <REDACTED>
  pool: 20

AI 回答(节选):*"PostgreSQL 单实例、日均 50 万写入、pool 仅 20,大概率是连接数不够。初步测试(内部 50 个真实 case 对比评估)显示,加入语义摘要后 AI 回答的相关性评分从 3.2 提升到 3.9(5 分制),可直接采纳率从 41% 提升到 63%。语义摘要的生成目前靠开发者在脱敏提示弹窗中手动补充关键上下文(一句话即可),后续计划结合内部 CMDB 和服务目录自动生成。

四条落地原则

回头看这三个月,最大的体会是:AI Coding 治理的核心难题不是技术,而是平衡。

这个平衡怎么把握?我们摸索出几条原则。

一、默认脱敏优于默认拦截, 能自动处理的别让用户手动操作。我们早期把内网 IP 设成直接拦截,结果一天触发上百次,开发者怨声载道。改成自动替换为 <INTERNAL_IP> 后,拦截量下降 90%,安全效果不变,开发者几乎无感知。

二、每一次拦截都要给出清晰的原因和替代方案。 不能只说"不行"不说"怎么办"。我们的拦截弹窗会明确告诉开发者"检测到内容与 payment-core 仓库高度相似",并提供"移除敏感片段后重试"或"提交审批流"两个选项。上线这个改动后,开发者对拦截的申诉率从 34% 降到了 8%。

三、安全策略要跟着数据走。 每周根据审计日志调整规则权重,不能拍脑袋定阈值。比如我们发现 .env 文件的误报率特别高(开发者贴示例 .env 模板也会被拦),就针对性地加了"是否包含真实值"的二次校验,误报率从 23% 一路降到了 3.8%。

四、前端只做第一层防线,别指望它解决所有问题。 后端校验和管理制度一个都不能少。前端的正则和指纹能被绕过吗?当然能——把代码 base64 编码一下就躲开了。但前端防线的价值在于拦住 95% 的无意识泄露,剩下 5% 的刻意绕过,靠后端深度检测和事后审计兜底。能落地的安全方案,一定不是最严格的那个,而是开发者愿意配合的那个。