Android Intent 重定向:攻击与修复
Intent 重定向如何让攻击者触及未导出的 Android 组件、通过 setResult() 泄露数据并滥用 PendingIntent,以及防范它的六种方法。
Intent 重定向是 Android 应用中的一类漏洞,恶意行为者借此诱使受害应用代其转发或分派一个 Intent。由于被转发的 intent 以受害应用的身份和权限执行,攻击者无需持有任何特殊权限,就能触及未导出的组件、窃取敏感数据或提升权限。
这类漏洞在 Android 漏洞赏金项目中一直被列为影响最大的发现之一,并曾波及包括 TikTok 和多款 Google 第一方应用在内的知名应用。
Intent 重定向的工作原理
攻击的核心在于利用一种代理模式(proxy pattern):受害应用从外部(受攻击者控制的)来源接收一个 intent,并在未经充分校验的情况下,使用该 intent 的一部分去启动另一个 activity、发送广播或绑定 service。
攻击流程

核心误用模式
最简单的脆弱代码如下所示:
// VULNERABLE — Unvalidated intent forwarding
Intent forward = getIntent().getParcelableExtra("next_intent");
startActivity(forward);
受害方盲目信任 next_intent 这个 extra,并启动攻击者指定的任意组件——包括受害方自己的未导出 activity。
脆弱的代码模式
1. 未经校验的 Intent 转发
// VULNERABLE
class RouterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val target = intent.getParcelableExtra<Intent>("target")
target?.let { startActivity(it) } // No validation at all
finish()
}
}
2. setResult() 数据泄露
// VULNERABLE — Returns internal data to the caller
public class LeakyActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent next = getIntent().getParcelableExtra("next");
startActivityForResult(next, 1001);
}
@Override
protected void onActivityResult(int req, int res, Intent data) {
super.onActivityResult(req, res, data);
// Forwards internal data straight back to the (attacker) caller
setResult(res, data);
finish();
}
}
3. 深度链接利用
// VULNERABLE — Deep link handler forwards without checking scheme/host
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data
val redirect = Intent(Intent.ACTION_VIEW, uri)
// Attacker can craft: myapp://redirect?url=intent://...
startActivity(redirect)
}
}
攻击场景
场景 1 — 访问未导出的组件
攻击者构造一个针对受害方未导出的 InternalSettingsActivity 的 intent:
Intent inner = new Intent();
inner.setComponent(new ComponentName(
"com.victim.app",
"com.victim.app.InternalSettingsActivity"
));
Intent outer = new Intent();
outer.setComponent(new ComponentName(
"com.victim.app",
"com.victim.app.RouterActivity" // exported proxy
));
outer.putExtra("target", inner);
startActivity(outer);
场景 2 — Content Provider 数据窃取
攻击者利用 URI 权限授权来读取受害方私有的 content provider:
val inner = Intent().apply {
data = Uri.parse("content://com.victim.app.provider/private_data")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
val outer = Intent().apply {
component = ComponentName("com.victim.app", "com.victim.app.ProxyActivity")
putExtra("next_intent", inner)
}
startActivityForResult(outer, 0)
// onActivityResult receives the private data
场景 3 — 身份验证 / 会话劫持
如果受害应用有一个已导出的 activity,它会从内部登录流程转发结果,那么攻击者就能截获通过 setResult() 返回的身份验证令牌。
场景 4 — 深度链接串联
攻击者跨多个应用串联多个深度链接,将每个应用作为跳板,最终触及某个高价值目标中的受保护组件。
PendingIntent 重定向
经典的 Intent 重定向利用的是:某个应用以自身身份转发攻击者提供的 Intent;而 PendingIntent 重定向则将攻击反转过来:受害应用被诱使交出一个 PendingIntent,而攻击者可将其武器化。由于 PendingIntent 会以创建者的 UID 和权限来执行其所包装的 Intent,获得可变(mutable)或空 PendingIntent 的攻击者实际上就借用了受害应用的身份。
理解 PendingIntent 的安全模型
PendingIntent 是一个能力令牌(capability token),而非数据对象。其中的 Intent、标志位和创建者 UID 都存放在 system_server 中;应用只持有一个 binder 句柄。
- 创建者(Creator):调用
PendingIntent.getActivity()→system_server存储该记录并返回一个句柄。 - 接收者(Recipient):通过 IPC(Intent extra、通知等)接收该句柄。
- 分派(Dispatch):当调用
.send()时,system_server查找该记录并以创建者的 UID(而非发送者的 UID)触发所包装的 Intent。

这种设计使得 PendingIntent 默认可以安全地四处传递——这也是它被推荐作为经典 Intent 重定向缓解措施的原因。只有当创建者把这个能力令牌当作无害的数据来对待时,漏洞才会出现。
脆弱模式
当受害应用出现以下情况时,就会发生 PendingIntent 重定向:
- 创建一个带
FLAG_MUTABLE的 PendingIntent(或在 Android 12 之前的目标上省略标志位,当时 mutable 是默认值),并且 - 包装一个隐式 Intent(未设置明确的组件),并且
- 将该 PendingIntent 暴露给攻击者——通过将其放入广播、通知操作、已导出 service 的响应,或返回给调用方的 Intent 中。
接收到可变 PendingIntent 的攻击者可以调用 .send(Context, int, Intent fillInIntent),并提供一个 fillInIntent 来补上缺失的组件、action、data 或 extra。系统会按照 Intent.fillIn() 中的规则将 fillIn 字段合并进原始 Intent,并以创建者的 UID 分派结果。
实际影响包括:
- 启动受害方的未导出 activity、service 或 provider。
- 通过 fillIn 授予
FLAG_GRANT_READ_URI_PERMISSION,从而读取或写入受害方私有的content://URI。 - 以受害方身份向那些信任受害方签名或包名的接收者发送广播。
- 触发特权内部流程(账户管理、设置更改、应用内购买回调)。
一个具体示例
// Vulnerable code inside VictimApp
Intent intent = new Intent(); // implicit — no component
PendingIntent pi = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Intent deliver = new Intent("com.victim.HAND_OUT_TOKEN");
deliver.putExtra("token", pi);
sendBroadcast(deliver); // attacker receives pi
攻击者的接收器获取该 PendingIntent 并加以利用:
// Inside AttackerApp
PendingIntent pi = intent.getParcelableExtra("token", PendingIntent.class);
Intent fillIn = new Intent();
fillIn.setClassName("com.victim", "com.victim.internal.AdminActivity");
fillIn.putExtra("cmd", "wipe");
pi.send(context, 0, fillIn); // launches VictimApp's internal activity as VictimApp
尽管 AdminActivity 并未导出,但启动仍然成功,因为系统是以 VictimApp 的 UID 来执行它的。
为何这与经典 Intent 重定向不同
| 方面 | 经典 Intent 重定向 | PendingIntent 重定向 |
|---|---|---|
| 攻击者提供的内容 | 向受害方提供一个原始 Intent |
什么都不提供——攻击者从受害方接收一个令牌 |
| 受害方的角色 | 提取一个 Intent 并分派它 | 交出一个包装了隐式、可变 Intent 的 PendingIntent |
| Intent 运行所依据的身份 | 受害方(混淆代理,confused deputy) | 受害方(能力委派,capability delegation) |
| 主要缓解措施 | 校验/过滤被转发的 Intent;改用 PendingIntent | 使用 FLAG_IMMUTABLE;使用显式 Intent |
造成的危害是一样的——代码以受害方身份执行——但攻击的形态是反转的。经典重定向需要一条输入路径,受害方在其中接受攻击者数据;而 PendingIntent 重定向需要一条输出路径,受害方在其中泄露了一项能力。
缓解策略
1. 使用 resolveActivity() 进行校验
在转发任何 intent 之前,先验证它解析到一个安全的、预期的组件:
val forwarded = intent.getParcelableExtra<Intent>("next")
forwarded?.let {
val resolved = it.resolveActivity(packageManager)
if (resolved != null && resolved.packageName == packageName) {
// GOOD — only allow intents targeting our own package
startActivity(it)
}
}
2. 组件允许清单
维护一组明确许可的目标组件:
private val ALLOWED_TARGETS = setOf(
"com.myapp.HomeActivity",
"com.myapp.SettingsActivity"
)
fun safeForward(intent: Intent) {
val target = intent.getParcelableExtra<Intent>("target") ?: return
val comp = target.component?.className
if (comp in ALLOWED_TARGETS) {
startActivity(target)
}
}
3. 使用 IntentSanitizer(AndroidX)
Jetpack 的 IntentSanitizer API 提供了一套声明式、构建器风格的 API,用于剥离危险字段:
val sanitizer = IntentSanitizer.Builder()
.allowComponent(ComponentName(this, HomeActivity::class.java))
.allowAction(Intent.ACTION_VIEW)
.allowDataWithAuthority("myapp.example.com")
.allowExtra("safe_key", String::class.java)
.build()
val clean = sanitizer.sanitizeByFiltering(untrustedIntent)
startActivity(clean)
4. 限制已导出的组件
检查您的 AndroidManifest.xml,确保只有在确实必要时才导出组件:
<!-- GOOD — not exported; cannot be reached externally -->
<activity
android:name=".InternalSettingsActivity"
android:exported="false" />
<!-- If exported is required, protect with a permission -->
<activity
android:name=".RouterActivity"
android:exported="true"
android:permission="com.myapp.permission.INTERNAL" />
5. 不可变的 PendingIntent
除非 PendingIntent 确实需要可变,否则始终使用 FLAG_IMMUTABLE:
val pi = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
6. 剥离危险的 Intent 标志位
在转发之前,移除可能授予 URI 权限的标志位:
fun stripDangerousFlags(intent: Intent): Intent {
intent.removeFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
)
return intent
}
常见误解
| 误解 | 实际情况 |
|---|---|
“设置 android:exported=false 就能让我的组件安全。” |
如果某个已导出的代理 activity 向它转发 intent,该组件实际上仍可被触及。 |
| “我校验了 intent 的 action,所以我受到保护。” | 攻击者控制着所有字段——组件、data URI、extra、标志位。仅校验 action 是不够的。 |
| “目标组件上的权限检查会拦住攻击者。” | 被转发的 intent 以受害应用的身份运行,而它本就持有所需的权限。 |
“只有 startActivity() 才脆弱。” |
sendBroadcast()、startService()、bindService() 和 startActivityForResult() 都可能受影响。 |
| “深度链接是安全的,因为它们只打开网页 URL。” | intent:// scheme 允许从一个 URI 构造任意 intent,从而绕过对 URL 的常规假设。 |
真实世界的影响
- TikTok(2022): 研究人员演示了一条 intent 重定向链如何通过触及处理身份验证令牌的未导出 activity 来劫持用户账户。该发现获得了可观的漏洞赏金。
- Google 安全公告: 多份 Android 安全公告都处理过系统级组件中的 intent 重定向缺陷,这凸显出即便是第一方代码也并非免疫。
- 漏洞赏金趋势: 在 HackerOne 和 Bugcrowd 等平台上,intent 重定向一直位列 Android 漏洞类别的前列,其赏金也反映出它的高影响。
结论:纵深防御
没有任何单一的修复能够消除 intent 重定向风险。分层的方法必不可少:
- 尽量减少导出——只导出那些确实需要外部访问的组件。
- 校验所有被转发的 intent——使用
resolveActivity()、组件允许清单或IntentSanitizer。 - 剥离危险的标志位——在转发前移除 URI 授权标志位。
- 使用不可变的 PendingIntent——默认采用
FLAG_IMMUTABLE。 - 保护敏感结果——绝不通过
setResult()向未经验证的调用方返回内部数据。 - 集成静态分析——Android Lint 和 Semgrep 等工具可以在发布前于 CI/CD 中捕获配置错误。
通过将每一个从外部接收的 intent 都视为不受信任的输入,并一致地应用这些控制措施,Android 开发者就能有效地将 intent 重定向作为一种攻击向量予以消除。