系统应用根据Uri授予权限的正确姿势

在我们印象中,Android6.0以后访问外部的媒体资源文件都是需要申请READ_EXTERNAL_STORAGE才可以正常访问,思考一个场景,假如我们不申请该权限,使用系统的Intent.ACTION_PICK意图跳转系统图库选取图片是否可以正常显示该图片?

答案是可以的,这是为什么呢?我们都没有申请权限,或者说是谁给了我们这个权限?带着这个疑问我们先来了解下UriPermission

UriPermission

Allow access on a per-URI basis

You can also grant permissions on a per-URI basis. When starting an activity or returning a result to an activity, set the Intent.FLAG_GRANT_READ_URI_PERMISSION, intent flag, the Intent.FLAG_GRANT_WRITE_URI_PERMISSION, intent flag, or both flags. This gives another app read, write, and read/write permissions, respectively, for the data URI that’s included in the intent. The other app gains these permissions for the specific URI regardless of whether it has permission to access data in the content provider more generally.

上面是Android官网的解释,大概意思是,你可以进一步对其他应用如何访问你应用的Contete Provider或者数据URI进行精细控制,可以通过读写权限来保护自己,根据 URI 授予权限。在启动 activity 或将结果返回给 activity 时,请设置 Intent.FLAG_GRANT_READ_URI_PERMISSION intent 标志、Intent.FLAG_GRANT_WRITE_URI_PERMISSIONintent 标志或者同时设置两者。这样便可针对 intent 中包含的数据 URI 分别向另一个应用授予读取权限、写入权限和读写权限。

理解完UriPermission,上面的问题就可以解释了,虽然我们没有申请读取的权限,但是系统图库在选图后将结果返回activity时设置了Intent.FLAG_GRANT_READ_URI_PERMISSION,这样我们就具有了读取该Uri指定图片的权限。

理想很丰满,现实很骨感。

背景

最近在项目中上线一款自研的图库应用(systemUid),支持系统默认选图action跳转,给调用者返回已选的Uri资源地址,因为安全合规整改的原因,一些第三方应用去掉了读写权限的申请,问题就被暴露出来了,第三方应用无法正常通过图库获取到图片资源。

分析

分析堆栈信息可以定位到,是调用者没有访问Uri的权限导致的异常,而我们自研图库在选图回传的时候是有设置FLAG_GRANT_READ_URI_PERMISSION,把URI的临时访问权限传递给调用者,且报错的堆栈打印是在第三方应用,所以可以初步判断问题应该是出自系统的权限授予过程。

我们可以通过context.grantUriPermission()作为切入点,来分析下系统是如何授予Uri权限

最终调用的是UriGrantsManagerService$checkGrantUriPermission()

  • checkGrantUriPermission

    int checkGrantUriPermission(int callingUid, String targetPkg, GrantUri grantUri,
            final int modeFlags, int lastTargetUid) {
       ....
        // Bail early if system is trying to hand out permissions directly; it
        // must always grant permissions on behalf of someone explicit.
        final int callingAppId = UserHandle.getAppId(callingUid);
        if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
            if ("com.android.settings.files".equals(grantUri.uri.getAuthority())
                    || "com.android.settings.module_licenses".equals(grantUri.uri.getAuthority())) {
                // Exempted authority for
                // 1\. cropping user photos and sharing a generated license html
                //    file in Settings app
                // 2\. sharing a generated license html file in TvSettings app
                // 3\. Sharing module license files from Settings app
            } else {
                Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
                        + " grant to " + grantUri + "; use startActivityAsCaller() instead");
                return -1;
            }
        }
        ....
    }
    复制代码
    

    问题就是出现在这里,如果你的应用是root用户,或者是具有系统级权限(systemUid),并且提供的Uri的authority不是指定的,就会拒绝授权 (return -1),也就是说这个Uri的权限并没有传递成功。这一点在上面的日志也有体现。

    /system_process W UriGrantsManagerService: For security reasons, the system cannot issue a Uri permission grant to

  • 系统为什么要这么做?

    注释里面有说,出于安全原因,系统应用不能使用startActivityAsCaller()来直接授予Uri权限,它必须显式地让应用自己授予权限。只有以下几种情况才会豁免授权

    1. 裁剪用户照片并共享生成的许可HTML文件的设置应用程序

    2. 在TvSettings app中共享生成的许可html文件

    3. 从设置应用程序共享模块license文件

调用者端

分析完图库端,我们再来看下调用者端的异常堆栈打印

客户端远程调用服务端打开指定文件,然后服务端把文件描述符跨进程传递到客户端(后面Binder驱动跨进程传递文件描述符就不展开分析)

通过分析时序图,可以定位到报错的堆栈信息是发生在enforceCallingPermissionInternal()

  • enforceCallingPermissionInternal()

    5636 private void enforceCallingPermissionInternal(Uri uri, boolean forWrite) {
                  ... // 省略部分代码
    5656          // First, check to see if caller has direct write access
    5657          if (forWrite) {
    5658              final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, table, null);
    5659              try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
    5660                  if (c.moveToFirst()) {
    5661                      // Direct write access granted, yay!
    5662                      return;
    5663                  }
    5664              }
    5665          }
                  ... // 省略部分代码
    5679          // Second, check to see if caller has direct read access
    5680          final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, null);
    5681          try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
    5682              if (c.moveToFirst()) {
    5683                  if (!forWrite) {
    5684                      // Direct read access granted, yay!
    5685                      return;
    5686                  } else if (allowUserGrant) {
    5687                      // Caller has read access, but they wanted to write, and
    5688                      // they'll need to get the user to grant that access
    5689                      final Context context = getContext();
    5690                      final PendingIntent intent = PendingIntent.getActivity(context, 42,
    5691                              new Intent(null, uri, context, PermissionActivity.class),
    5692                              FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
    5693 
    5694                      final Icon icon = getCollectionIcon(uri);
    5695                      final RemoteAction action = new RemoteAction(icon,
    5696                              context.getText(R.string.permission_required_action),
    5697                              context.getText(R.string.permission_required_action),
    5698                              intent);
    5699 
    5700                      throw new RecoverableSecurityException(new SecurityException(
    5701                              getCallingPackageOrSelf() + " has no access to " + uri),
    5702                              context.getText(R.string.permission_required), action);
    5703                  }
    5704              }
    5705          }
    5706 
    5707          throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
    5708      }
    复制代码
    

    在这个方法里面,它会根据参数forWrite判断当前调用者是否拥有读写权限,如果没有则会抛出异常提示,和上面报错的异常堆栈符合。

解决办法

以下两种方法都可以

  • 去除应用systemUid配置

  • 修改framework源码

考虑到修改系统源码的影响面比较大,所以采用去除systemUid的方式,再次验证跳转选图后可以正常加载。查看系统原生图库的清单文件配置,也是没有设置systemUid,原生图库也是采用了这种方式。

扩展

系统原生图库既不是系统应用,也没有动态申请存储权限,那它是怎么获取系统的存储权限的?

如果有了解过系统权限的授予流程,可以知道Android系统在开机后会对一些特殊的应用进行自动授权,而运行时权限的默认授予工作由DefaultPermissionGrantPolicy类的grantDefaultPermissions方法完成。
在这个方法里面可以看到它对图库进行默认授予存储权限的代码,具体是通过查找清单文件配置的category是Intent.CATEGORY_APP_GALLERY的应用。

作者:SugarTurboS
链接:https://juejin.cn/post/7138335779222355975
来源:稀土掘金

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值