9.4 KiB
Android移动安全第十五章_FragmentInjection
QIANXIN Team 来源:https://forum.butian.net/share/4946
系列目录:
- Android 组件导出安全
- Android Intent 安全
- Android Binder 服务安全
- Android ContentProvider 安全
- Android WebView 安全
- Android UI 欺骗与钓鱼
- Android Deep Link 安全
- Android 广播安全
- Android PendingIntent 安全
- Android 系统设置安全
- Android SSRF 与网络安全
- Android 加密与数据存储安全
- Android 认证与证书校验
- Android Zip Slip 路径遍历
- Android Fragment Injection(本章)
- Android SELinux 与沙箱机制
1. 前言
Fragment 是 Android 3.0(API 11)引入的 UI 组件。一个 Activity 可以包含多个 Fragment,每个 Fragment 管理自己的布局和生命周期。典型的使用场景是设置页面——左侧是分类列表,右侧根据选择加载不同的 Fragment 显示对应的设置项。
Android 提供了 PreferenceActivity 来简化设置页面的开发。PreferenceActivity 支持通过 Intent 的 extra 参数指定要加载的 Fragment 类名:
Intent intent = new Intent(this, SettingsActivity.class);
intent.putExtra(":android:show_fragment", "com.example.NetworkSettingsFragment");
startActivity(intent);
PreferenceActivity 收到这个 Intent 后,会用反射实例化指定的 Fragment 并显示。问题在于,如果 SettingsActivity 是导出的(设置页面通常需要导出,因为系统设置、快捷方式等需要跳转到它),外部 App 也可以通过这个参数指定任意 Fragment 类名。
2. 漏洞原理
2.1 PreferenceActivity 的 Fragment 加载机制
PreferenceActivity 在 onCreate() 中检查 Intent 是否包含 :android:show_fragment 这个 extra。如果有,就调用 Fragment.instantiate() 用反射创建该 Fragment 实例并加载到界面中:
String fragmentName = intent.getStringExtra(":android:show_fragment");
if (fragmentName != null) {
Bundle args = intent.getBundleExtra(":android:show_fragment_args");
Fragment f = Fragment.instantiate(this, fragmentName, args);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, f)
.commit();
}
Fragment.instantiate() 内部用 Class.forName(fragmentName) 加载类,然后调用无参构造函数创建实例。只要类名存在于 App 的 ClassLoader 中,就能被实例化。
2.2 攻击效果
App 内部可能有一些 Fragment 不打算对外暴露,比如:
- 调试信息 Fragment(显示内部日志、设备信息)
- 账户管理 Fragment(修改密码、绑定手机号)
- 高级设置 Fragment(开发者选项、隐藏功能开关)
- 数据导出 Fragment(导出用户数据)
这些 Fragment 本身没有导出的概念(Fragment 不是四大组件,不在 Manifest 中声明),它们的访问控制完全依赖于宿主 Activity。如果宿主 Activity 是导出的 PreferenceActivity,且没有校验 Fragment 类名,攻击者就能加载这些内部 Fragment。
攻击命令:
adb shell am start -n com.target.app/.SettingsActivity \
--es ":android:show_fragment" "com.target.app.internal.DebugFragment"
2.3 Fragment 参数注入
除了 :android:show_fragment,PreferenceActivity 还支持 :android:show_fragment_args 参数,用于向 Fragment 传递 Bundle 参数。Fragment 通过 getArguments() 获取这些参数:
public class DataExportFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null) {
String exportPath = args.getString("export_path");
}
}
}
攻击者不仅能指定加载哪个 Fragment,还能控制传给 Fragment 的参数。
3. isValidFragment() 校验
3.1 Android 4.4 的修复
这个漏洞在 2013 年被公开(CVE-2014-1710 是其中一个相关编号)。Android 4.4(API 19)在 PreferenceActivity 中添加了 isValidFragment() 方法作为修复:
protected boolean isValidFragment(String fragmentName) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.KITKAT) {
throw new RuntimeException(
"Subclasses of PreferenceActivity must override isValidFragment(String)"
+ " to verify that the Fragment class is valid!"
+ " " + this.getClass().getName()
+ " has not checked if fragment " + fragmentName + " is valid.");
}
return true;
}
PreferenceActivity 在加载 Fragment 之前会调用 isValidFragment(fragmentName)。如果 App 的 targetSdk >= 19,默认实现直接抛异常,强制开发者重写这个方法来校验 Fragment 类名。
3.2 正确的重写方式
public class SettingsActivity extends PreferenceActivity {
@Override
protected boolean isValidFragment(String fragmentName) {
return GeneralSettingsFragment.class.getName().equals(fragmentName)
|| NetworkSettingsFragment.class.getName().equals(fragmentName)
|| DisplaySettingsFragment.class.getName().equals(fragmentName);
}
}
白名单方式——只允许加载预期的 Fragment 类。
3.3 常见的错误实现
实际审计中,经常看到开发者为了消除异常而直接返回 true:
@Override
protected boolean isValidFragment(String fragmentName) {
return true;
}
或者用包名前缀做粗粒度校验:
@Override
protected boolean isValidFragment(String fragmentName) {
return fragmentName.startsWith("com.example.settings.");
}
这两种写法都没有真正解决问题。
4. 不依赖 PreferenceActivity 的 Fragment Injection
4.1 自定义的 Fragment 加载逻辑
不只是 PreferenceActivity,任何根据外部输入动态加载 Fragment 的代码都可能存在 Fragment Injection:
public class ContainerActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_container);
String fragmentClass = getIntent().getStringExtra("fragment_class");
if (fragmentClass != null) {
Fragment f = Fragment.instantiate(this, fragmentClass);
getFragmentManager().beginTransaction()
.replace(R.id.container, f)
.commit();
}
}
}
如果 ContainerActivity 是导出的,攻击者可以通过 fragment_class 参数加载任意 Fragment。
4.2 通过 Deep Link 触发
第七章讲过 Deep Link 的安全问题。如果 App 的 Deep Link 处理逻辑中包含 Fragment 加载:
Uri uri = getIntent().getData();
String fragmentName = uri.getQueryParameter("fragment");
if (fragmentName != null) {
Fragment f = Fragment.instantiate(this, fragmentName);
}
攻击者可以构造恶意链接诱导用户点击,触发 Fragment Injection。
4.3 AndroidX Navigation 组件
使用 AndroidX Navigation 组件的 App,如果 NavGraph 中的 deep link 配置不当,也可能导致类似问题。Navigation 组件根据 URI 匹配 destination(目标 Fragment),如果 URI 模式过于宽泛:
<fragment
android:id="@+id/settingsFragment"
android:name="com.example.SettingsFragment">
<deepLink app:uri="myapp://fragment/{name}" />
</fragment>
这里 {name} 是路径参数,传给 Fragment 作为 argument。虽然 Navigation 组件不会根据参数动态切换 Fragment 类(destination 是固定的),但参数值仍然是攻击者可控的,可能影响 Fragment 内部逻辑。
5. 版本演进
| 版本 | 变化 |
|---|---|
| Android 3.0 (API 11) | 引入 Fragment 和 PreferenceActivity 的 Fragment 加载机制 |
| Android 4.4 (API 19) | 添加 isValidFragment(),targetSdk >= 19 时默认抛异常 |
| Android 9.0 (API 28) | PreferenceActivity 被标记为 deprecated,推荐使用 AndroidX Preference |
| Android 13+ | AndroidX PreferenceFragmentCompat 成为主流,不存在 isValidFragment 问题 |
Android 4.4 的修复覆盖了 PreferenceActivity 的场景,但自定义的 Fragment 加载逻辑不受此保护。随着 AndroidX Preference 库的普及,PreferenceActivity 的使用在减少,但老应用和系统预装 App 中仍然常见。
AndroidX 的 PreferenceFragmentCompat 不使用 :android:show_fragment 机制,Fragment 的加载由 NavGraph 或代码显式控制,从架构上避免了这个问题。
6. 总结
Fragment Injection 的本质是信任了外部输入的类名。PreferenceActivity 的 :android:show_fragment 参数是最经典的入口,但任何根据外部输入动态实例化 Fragment 的代码都有同样的风险。
回顾一下:
- PreferenceActivity 根据 Intent extra 中的类名用反射加载 Fragment
- Android 4.4 添加了 isValidFragment() 校验,但开发者可能直接返回 true
- 自定义的 Fragment 加载逻辑不受 isValidFragment() 保护
- Fragment 没有独立的导出控制,访问控制完全依赖宿主 Activity
下一章讲 Android SELinux 与沙箱机制。SELinux 是 Linux 内核的强制访问控制模块,Android 从 4.3 开始引入,用于限制进程的权限范围。即使 App 获得了 root 权限,SELinux 策略仍然可以阻止它访问特定资源。