Expo SDK 安全评审机制解析:从命令注入到路径包含的七类漏洞防线

📅 发布时间:2026/9/7 3:32:51
Expo SDK 安全评审机制解析:从命令注入到路径包含的七类漏洞防线
Expo SDK 安全评审机制解析从命令注入到路径包含的七类漏洞防线【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expoExpo SDK 是一个发布到 npm 并被海量应用安装的 monorepo其 CI 中配置了一套强制运行的安全评审 Agent定义于 .expo-agents/code-review/agents/security.md围绕命令注入、生成文件注入、路径包含、开发服务器暴露、凭据与代码签名、原生平台暴露面和 CI 工作流七个维度规定了什么必须标记、什么不该标记的评审边界。读完本文你可以掌握 Expo SDK 中各类安全隐患的判定标准与已固化的修复模式如spawnAsyncargv 数组、adbShellArgs引号、tar 解包路径校验并能在自己的仓库中对照落实同样的防线。背景规则来自一次真实审计而非泛泛的清单该 Agent 文件以 frontmatter 声明alwaysRun: true即每次 PR 评审都运行并明确了一个关键事实仓库中每条规则都对应一个曾被真实修复的缺陷——2026 年 5 月一次内部审计以约 55 个 PR编号[EXP-01]至[EXP-67]落地修复文中引用的 PR 号如 #45819、#45848、#45967即其来源。因此重新引入这些形态的代码被视为对已修复工作的回归评审权重更高。Agent 的角色定位也决定了严重性判断标准packages/下的缺陷会直接发布给使用 SDK 的应用packages/expo/cli的缺陷则运行在开发者本机与他们的 CI 中。配套的协调器 .expo-agents/code-review/coordinator.md 进一步规定命令注入、或记录/打印/持久化密钥凭据的问题无论 PR 描述如何包装一律判为critical。命令与子进程构造argv 数组优先解释值必须可追溯判定标准需要标记的是packages/expo/**或tools/**中新增的、或解释范围被扩大的execSync、exec、spawnSync调用且解释值可能来自应用配置、文件名、设备输出或用户输入。规则刻意区分了存量与新增当前仓库中packages/expo/cli/src下仍存在有限的execSync调用例如 XcodePrerequisite.ts 中的execSync(open ${link})。这些调用本身不是发现项diff 仅仅移动既有调用也不是发现项——只有新引入的解释值才是。若确实必须走 shell每个解释值都要显式加引号。仓库中已固化的正确写法首选形式argv 数组 spawnAsync。tools/**下的 SPM 工具链就是因为注入风险被整体转换到这一模式#45819、#45837。例如 exportServer.ts 通过expo/spawn-async以参数数组调用子进程测试 installExpoPackage-test.ts 则用toHaveBeenCalledWith断言参数数组的精确形态防止未来回归为字符串拼接。adb shell的二次解析问题。adb shell会在设备端把尾参拼接后交给sh -c执行未加引号的元字符会在设备上直接执行。仓库给出的模式见 adb.ts/** * adb shell concatenates trailing args and runs the result through sh -c on * the device, so unquoted metacharacters in tainted tokens execute on-device. */ export function adbShellArgs(pid: Device[pid], ...command: string[]): string[] { return adbArgs(pid, shell, ...command.map(shellQuote)); } function shellQuote(value: string): string { return ${value.replace(//g, \\)}; }每个 token 独立加单引号、内部单引号以\转义。评审时凡是从应用配置、设备输出或用户参数拼进adb shell的 token都必须能指认这种逐 token 引号。AppleScript /sh -c必须复用统一转义工具。osascript/src/index.ts 提供两个助手escapeString反斜杠、双引号、\r\n\t转义用于 AppleScript 双引号字符串内和shellQuote单引号包裹用于 shell 上下文并在模块导出同文件 L213 附近中对外暴露供其他代码复用。绕过这两个助手直接拼接 AppleScript 或sh -c字符串即为发现项。其单测 index-test.ts 覆盖了引号、反斜杠、换行等转义边界可作为行为基准。生成原生文件与 HTML 的注入白名单校验与全量转义配置值进入原生工程文件应用配置、package.json、CLI 参数中的值被解释进生成的 Gradle、Xcode/SPM、Info.plist、AndroidManifest.xml或 Swift/Kotlin 源码时若没有校验白名单即为发现项。Bundle identifier、scheme 名、locale 字符串都曾因缺少白名单被修复#45884、#45888。暴露面集中在 packages/expo/config-plugins/src/ 和各包的插件目录各模块的plugin/子目录。仓库的 Android 模板 template-files/android/AndroidManifest.xml 中android:exported等属性的显式取值就是这类属性值受控约定的一个可对照样本。HTML 输出以下位置中用字符串拼接构建 HTML、或新增dangerouslySetInnerHTML除非每个解释值都经过转义都要标记expo/cli的中间件、router-server 的 html.ts、packages/expo/metro-config/src/serializer/、以及 expo-router 的静态渲染。最初被漏报的场景是资产 URL 被插入 HTML 属性#45848——URL 出现在属性上下文时与会闭合属性这是评审中要特别留意的注入点。路径包含前缀判断、归档解包与 URI 收敛这是规则中最具体的一类仓库内有可直接对照的实现范本。裸startsWith前缀判断写成裸resolved.startsWith(base)的包含检查会放行共享前缀的兄弟目录/data/foo与/data/foobar。正确做法是补一个path.sep或使用专用助手。仓库中的范本在 cli/src/utils/tar.ts先把输出根目录拼接为path.resolve(targetOutput) path.sep随后对每个解包条目path.resolve(output, name)后用带分隔符的根目录做startsWith校验越界条目直接跳过。归档解包写条目或跟随linkname符号链接前必须相对输出根目录重新解析。tar.ts 对SYMLINK/LINK分支的处理是标准模式case TarTypeFlag.SYMLINK: case TarTypeFlag.LINK: { const target path.resolve(parent, file.linkname ?? ); if (!target.startsWith(output) || target parent) { continue; } // ...link / symlink }此外创建父目录前会lstat检查父路径不是符号链接L70-L85防止通过父目录符号链接把写入引导到根目录之外。content:///file://URI 与配置提供根路径原生模块解析外部 URI 时若不 canonicalize 并收敛到应用的 scoped 目录即为发现项#45967、#45972、#45977对照实现见 FileSystemPath.kt。另一类是配置提供路径直接当文件系统根——EXPO_PUBLIC_FOLDER、routerRoot、模板路径、devtools 插件路径——若未断言其解析结果落在项目/包根目录内同样要报。外部提供的标识符联系人 id、EAS project id、包名未经校验或编码直接用作文件名/缓存路径片段属于同一类问题。开发服务器与请求处理origin、loopback 与无界循环新路由必须带 origin/loopback 校验packages/expo/cli/src/start/server/下新增的开发服务器路由、websocket 端点或 CDP 处理器若无 origin 检查isMatchingOrigin、assertSameOrigin或 loopback 检查isLocalSocket即为发现项原因是开发服务器对浏览器和局域网可达#45863。仓库中的既有实现可作范本createMessageSocket.ts 与 createDebugMiddleware.ts 均要求isLocalSocket(req.socket) isMatchingOrigin(req, serverBaseUrl)同时成立两个助手定义在 utils/net.ts。客户端驱动的无界循环无节流、无消息大小上限、无并发限制、无超时的客户端驱动循环必须标记——这些限制曾需要事后补进网络调试 websocket#45864。路由匹配与重定向expo-server或router-server的新路由处理器、RSC 端点若跳过方法与 header 强制、绕过共享中间件链、或在未命中时回退到更宽松的模块查找都要报#45905、#45870、#45895。URL/重定向处理若不在把输入当作同站路径之前拒绝协议相对//host或绝对外部目标即为发现项#45866对照实现为 expo-server/src/utils/matchers.ts 与 expo-router/src/link/href.ts。请求可控 header 的信任把Host、Forwarded、X-Forwarded-*、expo-platform等请求可控 header 用于主机解析、缓存键或路由选择除非值被约束到已知集合必须标记#48267、#45908。凭据、加密与代码签名文件权限token、会话、私钥写入时未显式设置 owner-only 文件模式即为发现项state.json与 expo-updates 私钥曾默认 world-readable#45873、#45880。带凭据的 fetch凭据 fetch 包装器或 auth header 附加前未校验请求目标主机与预期 API origin 一致#45875——否则中间人重定向即可把凭据送到他处。环境读取面expo/cli、expo/env、fingerprint 中安全相关配置API URL、凭据路径、工具链覆盖若经process.env读取.env文件就能供值。仓库为此保留了 original-system-env 访问器#45831、#45833评审时能用原始系统环境访问器而改用process.env即为发现项。expo-updates 代码签名对CertificateChain、CodeSigningConfiguration、SignatureHeaderInfo或更新请求 header 覆盖的改动若放宽校验、接受意外的证书链形态、或让签名检查变为条件性执行必须报。这些类型对应 expo-updates 的 CodeSigning 目录。OAuth/PKCEpackages/expo-auth-session/src/ 与 CLI 登录流程中缩短 verifier 熵、跳过state比较、放宽被接受的 redirect URI、或登出后会话仍有效都是发现项#45802、#44938。src/下的 PKCE.ts、AuthSession.ts 是评审时的对照对象。原生平台暴露面Android 组件导出AndroidManifest改动若导出 Activity/Service/Provider/Receiver或放宽networkSecurityConfig、允许 cleartext必须报告并点名变更后哪些东西变得可被设备上其他应用访问。仓库约定是android:exportedfalse加android:grantUriPermissions#45357、#44558模板清单文件 体现了这一写法。规则特别强调diff 里的解释、代码注释或 PR 描述不构成豁免——那些文字是作者可控的共享规则使其不具权威性确需导出的组件是留给人工评审判断的Agent 的职责是用发现项触发该判断。其他原生暴露packages/expo-dev-launcher/中冷启动即加载任意 dev-server URL 的 deep-link / intent-extra 处理使用计算键的对象合并或解析器输出未拒绝__proto__、constructor、prototype的写入——plist 解析曾需要这道防护#45854仓库中 plist 包 即为对应实现位置。CI 工作流fork PR 可达面github.event.*注入${{ github.event.* }}直接插进run:块或内联script:即为发现项——issue 标题、正文、分支名、login 都是攻击者可控的。仓库模式是经env:传递并加引号可对照 issue-triage.yml其 L147、L152 处的env:块即该模式的实例。触发器与 secrets新增pull_request_target、workflow_run、issue_comment触发器或 fork PR 可达的 job 中出现secrets.*引用必须标记约定是以非 fork 条件门控这些 job#45859。依赖CVE 史白名单对曾有 CVE 史、且低于工作区其他位置已锁定下限的包做新增或版本范围放宽要报。本树中已打过补丁的家族包括tar、node-forge、form-data、undici、axios、postcss、lodash、react-server-dom新引入的同族包同等对待。规则还点明了一个易被误判的前提.github/dependabot.yml 只声明了github-actions生态并对gradle/actions做了 major 版本忽略npm、gradle、CocoaPods 依赖并不在其监控范围——所以 diff 中直接编辑依赖声明值得认真对待不能以Dependabot 会覆盖为由放过。什么不该标记评审范围的纪律Agent 同样定义了负面清单防止误报淹没真正的问题工具链已强制的内容格式、lint、tsc严格性、SwiftLint、changelog 等见共享提示 .expo-agents/code-review/shared.md在本仓库无可达路径的理论漏洞——先追踪调用方若唯一调用方传字面量保持沉默刻意构造不安全输入的测试/fixture/示例代码除非该不安全助手被导出供真实使用——被称为 fixture本身证明不了什么要查路径与导出表按生产标准苛求apps/下的测试与演示应用它们的使命就是操练 SDK但其中的真实凭据泄漏仍算数已正确门控在__DEV__或开发构建检查之后的开发路径PR 未使其变差的既有缺失加固——你评审的是变更不是整个文件diff 未触碰的带 CVE 传递依赖——全仓库审计不是这个 Agent 的职责。报告纪律三个要素缺一不报规则的收尾给出了一条可执行的判定纪律点名攻击者、点名其可控输入、点名后果。若面前代码无法说清这三者就不要报告。一条实锤的 critical 发现胜过五条投机性的猜测。结合 coordinator 侧命令注入与凭据落地一律 critical的定级规则这套机制把高信噪比做成了双向约束对存量代码克制对新引入的可达漏洞零容忍。可落地的检查清单diff 中每个新 shell 调用值从哪来是否 argv 数组能否换成 spawnAsync 模式每个adb shell参数是否经过 adbShellArgs 式逐 token 引号每个startsWith路径判断前缀是否以path.sep结尾每个新 dev-server 端点是否同时有isLocalSocket与isMatchingOrigin每个密钥写入文件模式是否 owner-only每个 manifest 改动exported属性与可达组件是否点名每个 workflowgithub.event.*是否只出现在env:块中fork 可达 job 是否触碰secrets【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考