资讯详情

APP小程序源码敏感信息扫描实战:从正则规则到自动化闭环

📅 2026/10/21 9:25:56 | 华诺云谱 👁 阅读
APP小程序源码敏感信息扫描实战:从正则规则到自动化闭环
1. 为什么要专门扫描 APP 和小程序的源码1.1 前端代码是“半公开”的敏感信息重灾区做云上安全检查这几年我几乎每次都要把一个动作放在最靠前的位置把目标项目的APP安装包、小程序源码包拉下来喂给一个敏感信息递归扫描器。先说明白这不是多疑而是“前端代码里藏密钥”的命中率高得惊人。有一回在某客户的小程序包里光AccessKey就扫出来十多条其中两条还能直接关联到云上的存储服务另一次在APP解压后的JS bundle里发现硬编码的JWT令牌拿着这个令牌就能调后端用户信息接口。因为前端代码本质上属于“半公开资产”只要包能下载、能解包里面所有字符串都等于裸奔。很多开发团队会有个误解密钥只要不放在后端仓库放进前端代码就没事。实际上APP、小程序这类客户端包都要下发到用户设备上APK解压后可以看到assets、res目录小程序包解包后能看到JS、WXML、WXSS文件React Native的bundle文件更是整段明文JavaScript躺在里面。如果你把云厂商的AK写在某个API请求封装里把JWT直接塞进本地存储初始化代码里那这条密钥就相当于印在名片上发给了所有使用者。并不是开发者刻意作死而是本地调试时图省事或测试环境变量写死之后忘了拆这种事每天都在发生。除了直接写出密钥还有一种常见情况是“间接泄露”代码里保留了一个完整的调试接口地址带上某个固定Token就能访问内网数据。这种Token没有明显前缀单靠正则不好抓所以在扫描时除了匹配密钥本身还要把可疑的“上下文片段”都捞出来给人工复核。这也是我为什么一直坚持自己维护一套扫描脚本而不是完全依赖现成工具的最大原因真正的敏感信息很多时候不是一眼就能认出来的标准格式。1.2 扫描范围怎么定不是所有目录都要扫再说一个容易被新手忽略的环节扫描范围。第一次用这套脚本的人总想着把整个解包目录一锅端结果扫出来上万条“疑似手机号”其中大半是订单号、随机数字串根本没法看。我自己现在会按目标类型把范围收紧。小程序场景下重点扫解包后的主包、分包目录以及云函数代码目录如果存在泄漏的话。APP场景下重点扫APK或IPA解压后的根目录尤其是assets、res/raw、js、bundle等目录Flutter、RN这类跨端项目的JS bundle通常体积很大但里面往往藏着完整的业务逻辑和请求配置。配置文件类我单独划了一圈优先级包括.json、.properties、.env、.yml、.yaml、.php、.java、.js、.ts这类文本文件。同时必须明确哪些目录要跳过。最典型的是node_modules、.git、miniprogram_npm这些目录。第三方依赖包里的代码不是你写的扫出来的命中结果几乎全是噪音.git目录内部是压缩对象和二进制索引既不适合文本匹配又会拖慢速度。我在脚本里保留了一个SKIP_DIRS黑名单遍历时直接剪枝。这一步不是嫌麻烦而是为了让结果聚焦在“自己人写的前端代码”上减少人工复核的负担。注意扫描不是越全越好先把目录范围定清楚否则后面的报告会变成一场灾难。为什么强调“递归多层目录”因为真实解包后的目录深度比大多数人想象中夸张。一个APK解压后常见路径类似assets/rn/bundles/com/xxx/module/src/index.android.bundle层级轻松超过六层小程序分包解压后也有大量嵌套。手工打开文件夹一层层翻或者用编辑器逐个搜索效率极低还不忘漏。递归目录遍历的能力本质上是扫描器的地基地基不稳后面所有规则都白搭。2. 敏感信息类型与检测规则的设计思路2.1 五类核心敏感信息与正则规则表规则设计是整个扫描器最有含金量的地方。我不能直接把一堆正则堆给你因为如果规则过于宽松扫描结果等于废纸。先看我目前在用的五类核心规则表敏感信息类型常见格式特征风险级别判定思路云厂商AK各云厂商Key前缀如LTAI、AKIA、AKID等 定长随机串高前缀长度边界断言JWT令牌eyJ开头三段base64url用点号分隔高头段前缀三段式结构手机号1[3-9]开头11位数字中正则上下文关键词加权身份证号6位地区码8位生日4位顺序校验码高格式正则可选校验位算法私钥/AppSecret/数据库连接串私钥块标记、关键词随机字符串高关键词上下文字符集约束先说云厂商AK。各家AK格式虽然不同但都有比较明显的前缀和长度约束例如LTAI后面跟一串字母数字AKIA后面跟16位大写字母和数字AKID后面跟一长串字符。正则里用(?i)打开不区分大小写再用前后边界断言(?![a-z0-9])和(?![a-z0-9])卡死边界防止它作为更长字符串的一部分被误匹配。这一招能直接过滤掉很多没用的命中。JWT的正则设计核心是抓“三段式”。JWT的Header部分base64url解码后通常是{alg:HS256,typ:JWT}开头固定是eyJ这串字符整个Token结构是三段base64url字符中间用点号分隔。所以正则写成eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}比单纯匹配任意三段点号分隔字符串要稳得多。手机号和身份证的情况有点特殊。手机号的格式本身很简单1[3-9]开头加9位数字但这条规则单独使用会误报爆炸。订单号、时间戳、随机数字串里只要出现11位数字全都会被当成手机号捡出来。所以我把手机号命中分成两档如果行上下文里出现了mobile、phone、phoneNumber、contact这类字段名标为“中风险”否则只作为“低危线索”记录不进入主报告。身份证同理正则部分先做基本格式校验后面再讲可以加校验位二次确认。2.2 降低误报的关键前缀锚定与上下文加权如果你只是写一个正则然后往文件里怼那叫grep不叫敏感信息扫描。真正有实用价值的检测规则必须同时做三件事前缀锚定、长度约束、上下文加权。前缀锚定的意义和上面AK规则一样。比如身份证正则如果用\d{17}[\dXx]这种写法任何17位数字加一个尾号都会被扫出来乱码、哈希值、加密串全都会被误报。改成[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]之后相当于把地区码、出生年份、月份、日期全部做了一遍格式校验虽然还有理论上误报的可能但概率大幅下降。如果再配上身份证校验位计算基本可以滤掉绝大部分噪声。上下文加权是更进一步的优化。因为很多时候密钥本身并没有固定的“云厂商前缀”或者“JWT三段式”结构它就是一行appSecret K8mNpQ2X...这样的随机串。这时候单靠正则根本不知道该不该报警。我的做法是维护一个关键词列表包括secret、token、password、passwd、accessKey、access_key、apiKey、api_key等。当关键词命中了某一行就提取该行等号右侧的连续字符串用字符集约束只包含大小写字母、数字、若干特殊符号来判断它像不像一个密钥。长期跑下来我发现“上下文加权”比“堆更多正则”更有效。因为真实项目里的敏感信息形态太多了你不可能为每一种云服务、每一种框架的配置格式都写一条规则。把上下文作为重要的辅助信号能覆盖那些没有固定格式的泄密点。当然这也意味着结果里会有一些“可能敏感但不确定”的条目需要人工扫一眼但总比一条条去读几千行代码强。3. 递归多层目录扫描与文件处理实现3.1 目录遍历策略与循环目录避让目录遍历我直接使用Python的os.walk它本身就是递归实现可以一层层往下走。关键参数是followlinksFalse这一点很容易被忽略。如果目标目录里有符号链接指向某个上级目录而你又开启了跟随链接遍历会陷入无限循环最后把磁盘跑满。对于那些确实需要扫描链接目标的特殊场景我会改用手动维护已访问目录集合的方式每进入一个目录先取os.path.realpath把它放进一个Python集合如果遇到已经在集合里的目录就跳过。扫描过程中还要控制最大深度防止某些自动化解包程序生成异常深的目录把文件系统功耗拉满。我的默认设置是允许30层深度超过就停止递归。这个值对于正常项目完全够用因为多数目录层级都不会超过10层30层只是为了兜底一些极端嵌套的压缩包解压结果。遍历规则里还有个很重要的细节剪枝。在os.walk循环里直接处理dirnames列表把SKIP_DIRS里的目录名过滤掉例如node_modules、.git、.svn、miniprogram_npm等。这样做不只是为了减少误报也能大幅提高扫描速度。我见过有人把node_modules目录整个扫一遍几分钟都跑不完因为里面全是几万个JS文件实际上那些代码和自家业务没有任何关系。3.2 文件白名单、编码判定与大文件保护遍历到文件后第一件事不是立即读内容而是先做一轮“值得扫”判断。我会维护一个文本扩展名白名单包括.js、.ts、.json、.html、.htm、.xml、.txt、.log、.md、.vue、.wxml、.wxss、.php、.py、.java、.properties、.yml等。白名单之外的文件比如图片、字体、压缩包、二进制so库直接跳过。扩展名白名单是效率基础但不是万无一失。有些配置文件没有扩展名比如.env文件实际名是.env没有后缀还有些从服务器上下载的源码文件后缀混乱。所以我还会做二次探测打开文件读取最前面2KB字节如果里面包含\x00这种空字符就判定为二进制文件并跳过。这套“扩展名头字节探测”的组合覆盖了我遇到的大部分文本文件场景而且性能消耗很小。编码处理是另一个容易翻车的点。很多老旧项目的配置文件是GBK编码如果你强行用utf-8解码轻则乱码重则直接抛异常导致扫描中断。我现在的读取逻辑是按顺序尝试utf-8、gb18030两种编码都用try/except UnicodeDecodeError包裹处理。如果都失败最后用utf-8加errorsignore兜底。gb18030是GB2312和GBK的超集能覆盖绝大多数中文编码场景用这一个就够。大文件保护也很关键。bundle文件动辄几十MB如果你一次性read()进内存小机器直接卡死。我的做法是把最大文本文件大小默认设置为20MB超过就跳过但某些RN或Flutter的bundle会超过这个门槛所以我把大小阈值设计成可配置参数遇到大项目时调高。避免一次把整个文件读入内存的另一个办法是流式逐行读取但逐行读取会在正则匹配跨行上下文时受限所以我在效率和覆盖之间默认选择阈值跳过扫描结果里会明确标出“因文件过大未扫描”避免误以为自己扫得很干净。4. 核心扫描实现与报告输出4.1 规则管线与命中去重扫描核心的管线逻辑其实很清晰遍历文件 - 读取文本 - 预编译正则逐条匹配 - 命中后提取行号和上下文 - 计算风险等级 - 脱敏 - 汇总结果。我贴一个精简版可直接运行的示例核心参数都在里面你自己可以根据项目情况调整。import os import re import json from pathlib import Path TEXT_EXTS {.js, .ts, .json, .html, .htm, .xml, .txt, .log, .md, .vue, .wxml, .wxss, .php, .py, .java, .properties, .yml, .yaml, .env} SKIP_DIRS {node_modules, .git, .svn, miniprogram_npm, __pycache__} MAX_FILE_SIZE 20 * 1024 * 1024 MAX_DEPTH 30 RULES [ { name: cloud_access_key, risk: high, regex: r(?i)(?![a-z0-9])(?:LTAI[a-z0-9]{12,20}|AKIA[0-9a-z]{16}|AKID[a-z0-9]{13,20})(?![a-z0-9]) }, { name: jwt_token, risk: high, regex: reyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,} }, { name: mobile_number, risk: medium, regex: r(?![0-9])1[3-9]\d{9}(?![0-9]) }, { name: id_card, risk: high, regex: r(?![0-9])[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?![0-9]) }, { name: private_key_block, risk: high, regex: r-----BEGIN (?:RSA|EC|OPENSSH|DSA|PRIVATE) PRIVATE KEY----- }, ] CONTEXT_KEYWORDS [secret, token, password, passwd, accesskey, access_key, apikey, api_key] def is_binary(raw: bytes) - bool: return b\x00 in raw[:2048] def load_text(path: Path): try: raw path.read_bytes() except OSError: return None if len(raw) MAX_FILE_SIZE or is_binary(raw): return None for enc in (utf-8, gb18030): try: return raw.decode(enc) except UnicodeDecodeError: continue return raw.decode(utf-8, ignore) def mask_phone(phone: str) - str: return phone[:3] **** phone[-4:] def mask_idcard(idcard: str) - str: return idcard[:6] ******** idcard[-4:] def scan_text(path: Path, text: str): lines text.splitlines() hits [] for line_no, line in enumerate(lines, start1): lower_line line.lower() for rule in RULES: for match in re.finditer(rule[regex], line): value match.group(0) ctx_flag 1 if any(k in lower_line for k in CONTEXT_KEYWORDS) else 0 risk rule[risk] if rule[name] mobile_number and ctx_flag 0: risk low if rule[name] mobile_number: value mask_phone(value) if rule[name] id_card: value mask_idcard(value) hits.append({ pattern: rule[name], risk: risk, file: str(path), line: line_no, match: value, context: line.strip()[:200], }) return hits def main(root: str): root_path Path(root) all_hits [] files_scanned 0 for dirpath, dirnames, filenames in os.walk(root_path, followlinksFalse): dirnames[:] [d for d in dirnames if d not in SKIP_DIRS] depth Path(dirpath).relative_to(root_path).parts if dirpath ! root_path else () if len(depth) MAX_DEPTH: dirnames[:] [] continue for filename in filenames: p Path(dirpath) / filename if p.suffix.lower() not in TEXT_EXTS: continue text load_text(p) if text is None: continue files_scanned 1 all_hits.extend(scan_text(p, text)) summary {} for h in all_hits: summary[h[pattern]] summary.get(h[pattern], 0) 1 report { scan_root: str(root_path), files_scanned: files_scanned, total_hits: len(all_hits), summary: summary, hits: all_hits, } return report if __name__ __main__: result main(/path/to/your/source) print(json.dumps(result, ensure_asciiFalse, indent2))这个脚本的规则表我刻意放在文件顶部方便你增删。scan_text函数的核心是逐行匹配每命中一个规则就把文件路径、行号、脱敏后的匹配值以及上下文片段记录下来。上下文片段非常重要因为人工复核时不可能每次都重新打开源文件报告里直接能看到“这段代码大概在做什么”。命中去重我放在汇总阶段处理。同一个文件同一行可能被多条规则命中例如一行配置里同时出现手机号和身份证号那就应该记录两条不能合并。同一行同一个正则连续出现多个匹配值例如数组里列了十多个手机号我按每个匹配值分别记录但报告里会压缩上下文避免一个数组就把报告撑爆。4.2 成果输出JSON 报告与统计摘要输出格式我选JSON不用CSV。原因很简单JSON能嵌套展示上下文、文件路径、行号、风险等级这些结构化字段后续接入告警平台、导入其他工具都很方便。CSV虽然能用Excel打开但字段一多就是灾难尤其上下文里可能包含逗号和换行会让表格乱掉。报告里必须带上统计摘要也就是每个规则命中多少次。不要小看这个汇总它能直接告诉你这个项目最严重的问题是什么。如果一个包扫出50条JWT令牌但AK只有1条说明这个团队的鉴权体系可能整体都暴露了而不只是某个配置文件写错。如果手机号命中数极高但高优命中很少说明这套代码里有大量个人信息在流转本身就值得警惕。我对不同风险等级的处理策略是高优命中AK、私钥、JWT、身份证立刻拉群通知负责人中、低风险放进周报汇总。报告落地后手机号和身份证必须保持脱敏状态。这一点非常重要因为报告本身可能被转发、归档如果里面躺着一批明文身份证号那扫描器反而成了新的数据泄露源头。5. 实操中的高发问题与排查记录5.1 五个容易“翻车”的场景这套方案跑了两年多我遇到过的问题基本可以浓缩成一张速查表现象原因对策JWT规则命中乱码base64串部分base64编码数据恰好以eyJ开头对头段做base64url解码确认能否解出JSON手机号扫出一堆订单号、金额正则只匹配11位数字增加上下文关键词提升风险否则降级为低危线索AK命中开源示例代码文档或示例中写了固定占位符AK跳过示例目录维护已知占位符名单大bundle扫描极慢或内存溢出全量读入超大文本文件设置大小阈值或改为流式逐行读取GBK中文文件全部漏报默认utf-8解码失败后直接跳过增加gb18030兜底解码先展开说下JWT误报。base64编码的任意图片、密文、序列化数据如果开头两个字符刚好是eyJ又恰好被点号分成三段就会被当JWT扫出来。我自己遇到过一个案例一个配置文件里藏了一长串base64编码的加密数据扫描器报了JWT人工复核时才发现完全不是。降低误报的方法是在命中后再写一个校验函数把第一段尝试用base64url解码看解码结果是不是以{开头且包含alg字段。这个校验逻辑不复杂但能把误报率降低一个数量级。手机号误报是我日常最头疼的问题。前端代码里的订单号、流水号、假手机号测试用例非常多。我的处理方式是分档匹配到手机号后如果单行上下文里有mobile、phone、contact、phoneNumber这类字段名就标记为“中风险”没有这些关键词时仅作为“低危线索”进入附录清单不进主报告。这样既不会漏掉真正的个人信息又不会让主要结论淹没在噪声里。5.2 性能优化手段扫描器最大的性能瓶颈在于大目录里的海量文件遍历和文本读取。我有几个常用的提速手段。一是多进程。Python的concurrent.futures.ProcessPoolExecutor可以按目录或按文件列表分片并行处理在多核机器上速度能提升三到五倍。要注意的是多进程模式下每个子进程各自维护正则预编译结果所以正则对象尽量做到模块级复用避免反复编译。二是先过滤后读取。前面讲的扩展名白名单和二进制头探测能挡掉大部分无意义文件。很多APK解压目录里图片、音频、字体文件占了大多数这些连打开都不需要直接跳过。三是命中后的二次确认。对AK这类规则可以再做一次前缀细分把命中结果按厂商分组方便报告按优先级呈现。如果扫描对象是固定的一批定期任务还可以把扫描过的文件哈希存下来内容没变就直接跳过做成增量扫描。特别提醒报告里所有手机号、身份证展示必须脱敏报告本身也可能被再次传阅不能把扫描器变成新的泄露源。6. 扫描结果如何落地从发现到验证再到闭环6.1 命中结果的人工复核与风险确认扫描器给你一份命中列表这只是开始。真正的工作量在于人工复核。我自己的复核流程是这样看上下文片段确认这个密钥是不是真实业务代码里正在使用的而不是测试用例、历史遗留注释或示例代码。如果上下文里有硬编码的AK下一步不是复制它去调什么接口而是立刻确认供应商控制台里有没有异常操作然后走正常流程轮换该密钥。密钥轮换这个动作千万别省。有人发现前端代码里藏着AK以为把代码删掉重新发布就完事了。但旧包可能已经在线上保留了一段时期或者被其他渠道下载过删除源码只是阻止了进一步扩散已经暴露的密钥依然可以继续使用。正确的做法是到云控制台禁用、删除或轮换这把AK再处理代码本身。这条补丁逻辑适用于所有高优命中JWT要换签发密钥数据库连接串要改密码手机号身份证要评估是否涉及合规上报。对于中低风险命中也不能直接无视。手机号这类个人信息的暴露意义不在“能不能用来登录”而在于这本身就是合规问题。如果前端代码里存在一批明文手机号和身份证号通常意味着某个接口、某个文件、某个日志在某个环节上把个人信息落到了不该落的地方。顺着命中的上下文继续追踪往往会发现更深的问题比如某个调试接口未鉴权、某个日志文件被直接打包进了安装包。6.2 把扫描器接进日常版本的发布流程扫描器最合适的定位不是“出事之后再跑”而是“发布之前就拦”。把扫描逻辑做成独立的命令行工具后可以接进CI流水线。常见做法是在前端构建完成、生成安装包或小程序产物后自动对产物目录执行一次扫描。如果出现高优命中构建失败或触发告警中低风险问题则允许构建通过但自动把报告发给相关负责人。我见过一些团队直接拿扫描失败卡住发布流程结果因为规则误报让正常上线被阻断开发同学抱怨不断。所以我的建议是区分“阻断项”和“提示项”。AK、私钥、JWT这类高优命中建议阻断手机号、身份证这类需要结合上下文的规则先走提示不要一刀切。等规则库稳定了再把误报率降到足够低的常见模式提升为阻断项。扫描结果接上企业内部的IM告警群、工单系统才算真正形成闭环。最后说一点非常碎但很实在的经验敏感信息扫描这件事最忌讳“扫完发完报告就结束”。我见过有团队用脚本扫出了几十条AK发了一份漂亮的报告然后就没有然后了。几个月后再看那些密钥原样躺在线上包里风险一点没减少。我自己现在维护的这套扫描器每个月固定跑一遍全部线上小程序包和APP包发现新增命中就盯到负责人完成密钥轮换。扫描是一种习惯不是一次性任务。与其追求一个完美无瑕的规则库不如先把“定期扫描人工复核强制闭环”这套节奏跑起来。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑