工程文章

云端 Mac 上审计 Xcode 构建产物中的绝对路径泄露

云端 Mac 上审计 Xcode 构建产物中的绝对路径泄露

一条归档流水线可能已经稳定运行数月,却仍把构建账号、仓库目录甚至临时工作区名称写进产物。常见痕迹包括 /Users/runner/DerivedData、挂载卷路径和源码文件的完整位置。它们通常不会直接改变 App 行为,却会向拿到安装包、符号文件或日志的人暴露内部结构。云端 Mac 会被不同任务反复使用,更应该把路径检查做成发布前的固定步骤,而不是偶尔执行一次字符串搜索。

先划清要检查的产物边界

一次 Xcode 归档至少要区分三类对象:准备分发的 .app,留在团队内部的 .dSYM,以及流水线日志。风险不能混在一起判断。

可分发 App 中出现真实用户目录,应当按高优先级处理。dSYM 记录源码路径是调试信息的一部分,不宜看到路径就删除;真正的问题是它被错误公开,或者路径包含不该传播的任务标识。日志的暴露面取决于读取权限和保存期限,但仍不应长期保留临时凭据目录与个人用户名。

对象 典型路径来源 建议动作
App 可执行文件 断言、调试字符串、生成代码 命中真实本机路径时阻断
dSYM DWARF 编译目录与源码路径 映射前缀并复验符号化
构建日志 命令回显、脚本、工具输出 脱敏后限制保存期限
测试附件 截图、诊断包、结果包 单独检查后再归档

路径审计的目标不是消灭所有调试信息,而是避免把构建机器的真实目录结构带入不需要它的分发边界。

从干净归档开始扫描

先固定归档路径,避免扫描脚本自己制造随机目录。Release 配置应由流水线显式传入,不要依赖开发机上一次选择的 Scheme 状态。

set -euo pipefail

ARCHIVE="$PWD/output/App.xcarchive"

rm -rf "$ARCHIVE"
xcodebuild archive \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$ARCHIVE"

定位 App 的主可执行文件后,使用系统自带的 strings 搜索高价值特征。除了 /Users/,还应检查 /Volumes/DerivedData.xcworkspace 和团队实际使用的临时目录前缀。

APP="$ARCHIVE/Products/Applications/App.app"
EXEC=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$APP/Info.plist")
BIN="$APP/$EXEC"
REPORT="$PWD/output/app-paths.txt"

strings -a "$BIN" |
  grep -E '/Users/|/Volumes/|DerivedData|\.xcworkspace|\.xcodeproj' \
  > "$REPORT" || true

if [ -s "$REPORT" ]; then
  cat "$REPORT"
  exit 1
fi

主程序通过不代表整个包干净。还要遍历嵌入式 framework、扩展和资源文件。扫描报告应记录“文件路径、匹配内容、产物类型”,但不要把整份二进制或环境变量直接上传到日志。

单独检查 dSYM

dSYM 的检查重点是 DW_AT_comp_dir 与源码文件记录,而不是普通字符串数量。先找到 DWARF 文件,再输出可能指向真实工作区的条目。

DSYM="$ARCHIVE/dSYMs/App.app.dSYM"
DWARF="$DSYM/Contents/Resources/DWARF/App"

xcrun dwarfdump --debug-info "$DWARF" |
  grep -E 'DW_AT_(comp_dir|decl_file).*("/Users/|"/Volumes/)' \
  > "$PWD/output/dsym-paths.txt" || true

xcrun dwarfdump --uuid "$DWARF"

UUID 要与归档中的可执行文件对应。若不一致,即使路径处理正确,后续崩溃地址也无法可靠还原。

用编译前缀映射收敛真实目录

Swift 编译器可通过 -debug-prefix-map 把工作区前缀替换为稳定的逻辑目录;Clang 使用 -fdebug-prefix-map。在 Xcode 的 Release 配置中可分别加入:

OTHER_SWIFT_FLAGS = $(inherited) -debug-prefix-map $(SRCROOT)=/src
OTHER_CFLAGS = $(inherited) -fdebug-prefix-map=$(SRCROOT)=/src
OTHER_CPLUSPLUSFLAGS = $(inherited) -fdebug-prefix-map=$(SRCROOT)=/src

不要直接把整个 /Users 映射掉。过宽的规则可能掩盖外部依赖来源,也会让不同目录意外落到同一逻辑路径。优先映射 $(SRCROOT);若依赖检出目录固定,再为该目录增加独立规则。

映射后必须重新归档,不能只修改构建设置后复用旧的 DerivedData。还要确认编译命令确实带上参数,因为某些自定义脚本会绕开工程设置直接调用编译器。

把误报分级而不是全部放行

扫描经常命中资源中的示例路径、测试夹具或第三方调试文本。处理方式应基于位置和可达性,而不是维护一份不断扩张的全局忽略词。

建议设置三级结果:

  1. 分发 App 或嵌入式组件出现当前构建机真实前缀,直接失败。
  2. dSYM 出现未映射源码前缀,标记失败,但保留文件供诊断。
  3. 内部日志、测试结果包出现路径,生成告警并检查访问范围。

白名单应精确到“文件 + 正则 + 原因”。例如某个测试资源确实需要展示 /Users/example/,可以只豁免该资源;不要把 /Users/ 整体加入忽略列表。流水线还应把当前工作区路径作为动态禁止项,这比猜测所有用户名更可靠。

避免扫描脚本泄露更多信息

报告中可将真实前缀替换为 $WORKSPACE,仅保留相对路径和命中类型。命令失败时也不要打印完整环境。若需要留存原始报告,应把它作为受限构建附件,而不是直接展开到所有成员都能读取的控制台输出。

发布前复验符号化能力

路径映射可能通过扫描,却不代表调试链路仍然完整。发布前至少保存 App、dSYM、UUID 清单和构建提交标识,并选取一个真实指令地址执行符号化测试。验证结果应能返回函数名和源文件逻辑路径 /src/...,而不是只有十六进制地址。

最终门禁可以归纳为四项:App 内无真实工作区前缀;dSYM UUID 与二进制一致;映射后的源码路径可识别;日志中不展开原始扫描内容。这样既缩小目录信息的暴露面,也保留定位线上故障所需的证据。

路径审计适合在 VMOrbit 云端 Mac 的归档任务后立即运行。它不依赖固定用户名,也不要求修改源码目录结构;只要工作区根路径由流水线统一提供,同一套规则就能覆盖临时任务与长期构建节点。

常见问题

App 中出现 /Users/ 路径就一定构成安全漏洞吗?

不一定,但它会暴露构建用户名、目录结构或项目命名。应先确认路径位于可分发 App、调试符号还是内部日志,再按暴露范围分级处理。

使用 debug-prefix-map 后还需要保留 dSYM 吗?

需要。前缀映射只改变调试信息中的路径表示,不替代 dSYM。发布前应保存 dSYM,并用真实地址执行一次符号化复验。

路径扫描应该阻断所有构建吗?

建议只让可分发 App 中的真实本机路径直接阻断;dSYM 和内部日志先进入告警与人工复核,避免因调试信息的合理路径记录误伤发布。

独享物理节点

把构建任务放到独享云端 Mac

选择 VMOrbit M4 或 VMOrbit M4 Pro,并按团队位置选择节点。实际可用性与交付信息以控制台实时返回为准。

选择云端 Mac