京东金融 App 收集隐私?开源库程序员不背锅!

640?wx_fmt=gif

「大数据时代,人人都在“裸奔”」。本是一句玩笑话,无奈现实却一次又一次地扯掉了遮挡在用户隐私面前的遮羞布。

近日,京东金融 App 成为众矢之的,原因是其被网友控诉,在用户不知情的情况下,App 会获取用户截图及照片,窃取隐私。而这究竟是怎么一回事?


640?wx_fmt=png

技术宅的发现


追溯事件本身,还得从一位技术爱好者、业余开发者、微博博主@瘦出的肋骨已经消失的大侠阿木 发布的一则微博提起。

昨天凌晨,该博主在微博上发视频控诉,“京东金融 App 会获取用户的敏感图并上传。”

640?wx_fmt=png

根据该博主发布的视频显示,测试流程如下(仅在  Android 端):

  • 打开京东金融 App 后,退回到桌面,让该 App 在后台继续运行;

  • 打开另一款招商银行 App,对该 App 任意页面进行截图;

  • 随后打开手机中文件管理器,找到京东金融的文件目录,打开缓存文件后,竟然发现了此前对招商银行 App 页面的截图(android/data/com.jd.jrapp/cache/uil-images)。

紧接着在 20 分钟后,@瘦出的肋骨已经消失的大侠阿木 再次发微博视频表示,“京东金融 App 不止偷截图,还会偷照片”。在此次视频演示中,他同样先打开了京东金融 App,随后打开美颜相机,并使用美颜相机拍摄一张照片保存,如其预料的一样,在上述京东金融 App 的缓存目录下也找到了刚刚摄下照片。

此消息一经爆出,立即引发部分京东金融 App 用户恐慌,甚至也有不少用户称:“复现成功。”对此,在评论区,网友们也展开激烈讨论:

  • 应该是给给了访问相册的权限吧,禁掉权限应该就拿不到图了;

  • 不止京东,国内很多 App 都会把用户隐私按在地上摩擦;

  • 先要确认是不是官方 App 的行为,这个要等官方解答,但作为一个安全从业者,Android 生态上的 App 分发平台及 App 自身的权限管控安全问题确实亟需关注,截屏、麦克风、摄像头权限都是重灾区。

事实上,针对该事件,存在一个最为争议的问题就是,该博主表示的“获取用户的敏感图片并上传”问题。倘若在未经明确告知用户的前提下,将获得的图片上传至京东的服务器端,那么该 App 窃取用户隐私几近实锤了。所以京东金融 App 所获取的用户截图及照片,究竟是保存在了用户个人手机的本地端?还是上传到了服务器端?


640?wx_fmt=png

技术宅的深挖


基于这一问题,一位软件开发者、知乎作者@琴梨梨(https://zhuanlan.zhihu.com/p/56875556,已获作者授权通过反编译代码,对此事件进行了一波简要分析:

以下为解析正文(在不改变原意的基础上,对文字稍做修改):

根据微博网友@瘦出的肋骨已经消失的大侠阿木 爆料的视频中,我们可以看到一个名为 uil-images 的文件夹,接下来,直接进 dex 挖字符串:

640?wx_fmt=png

从中,我们发现其全部来自一位昵称为 nostra13 的开发者的开源库。顺藤摸瓜,再去 GitHub 上搜索详情:

640?wx_fmt=png

该开源项目获得的 Star 还挺多多嘛,按照道理不应该是开源库的锅啊。

接着爬调用开源库的类并过滤关键词 screenshot 找到了京东金融 App 捕获截图功能代码的这个位置:Lcom/jd/jrapp/bm/common/screenshot/

640?wx_fmt=jpeg

从中可以看到一个接收器。接下来,拿着这个接收器倒爬,发现在 activity 基础类里注册了接收器。

640?wx_fmt=jpeg

其中包括一个 startlisten 方法,再次返回去找这个方法:

640?wx_fmt=jpeg

这个方法很简短,但里面这个 businessshot 为什么要带个 business 字样呢?有点耐人寻味。

接着去找这个方法:

640?wx_fmt=jpeg

就一个 iput。因为 iput 要求附加 context,所以肯定存在 iget 且仅存在于这个类里(包括附属类)。

在这个类里搜索变量,发现只有 onShot 方法里有:

640?wx_fmt=jpeg

但是按照道理这个方法应该是截屏后去调用的,前面这个注册接收器怎么注册的还是有点迷。

返回基础 activity 类翻,发现这个:

invoke-virtual {v0, v1}, Lcom/jd/jrapp/bm/common/screenshot/ScreenShotListenManager;->setListener(Lcom/jd/jrapp/bm/common/screenshot/ScreenShotListenManager$OnScreenShotListener;)V

看样子这个 manager 类里有问题。

后来发现一个新变量,接着去爬这个变量:

640?wx_fmt=jpeg

然后发现这个 manager 里还真有点东西:

640?wx_fmt=jpeg

出现在方法 handleMediaRowData 里。再深度探究这个方法,发现又在 handleMediaContentChange 这个方法里。继续往下,发现这个 access 方法:

640?wx_fmt=jpeg

其中这个方法的 p1 是个 uri。到这里,感觉已经快接近真相了。

果真,在一个附属类里找到了这个 access 的引用:

640?wx_fmt=jpeg

类名 ScreenShotListenManager$MediaContentObserver,似乎是监控媒体变化。

编译这个监听器,找 init:

640?wx_fmt=jpeg

找到了 init,在 startListen 方法里。

至此为止,listener 部分分析结束。

接下来要做的是分析这个接收器到底调用开源库做了什么。关键代码在:

sget-object v2, Lcom/jd/jrapp/library/imageloader/ImageOptions;->commonOption:Lcom/nostra13/universalimageloader/core/DisplayImageOptions;

invoke-virtual {v1, p1, v0, v2}, Lcom/jd/jrapp/library/imageloader/JDImageLoader;->displayFile(Ljava/lang/String;Landroid/widget/ImageView;Lcom/nostra13/universalimageloader/core/DisplayImageOptions;)V

这个 displayFile 方法有点蹊跷。

追根溯源,我们发现在 JDImageLoader 里面调用的这个方法如下

.method public displayFile(Ljava/lang/String;Landroid/widget/ImageView;Lcom/nostra13/universalimageloader/core/DisplayImageOptions;)V
.registers 6
.prologue
new-instance v0, Ljava/lang/StringBuilder
;
invoke-direct {v0}, Ljava/lang/StringBuilder;->()V
const-string/jumbo v1, "file://"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
//截止到现在都是在获取uri
invoke-virtual {p0, v0, p2, p3}, Lcom/jd/jrapp/library/imageloader/JDImageLoader;->displayLocalImage(Ljava/lang/String;Landroid/widget/ImageView;Lcom/nostra13/universalimageloader/core/DisplayImageOptions;)V
//用开源库展示
return-void
.end method

似乎截止到现在,我们也并没有发现 App 的操作出现攻击性代码,看方法名判断是仅仅获取截图并显示。

但是显示之后为什么文件就跑京东金融 App 私有目录里去了?

初步判断和开源库脱不开关系。

于是,继续往下研究,当看到 displayLocalImage 这个方法,我们去找找:

640?wx_fmt=jpeg

从中,我们发现调用了另外一个 displayLocalImage 方法。

对此,笔者不禁强烈谴责这种俄罗斯套娃式写法,点名批评!!!

接着我们打开这个套娃看看:

640?wx_fmt=jpeg

其中,调用了开源库 displayImage 方法。

精确定位,发现又是个套娃:

640?wx_fmt=jpeg

看来,这套娃还不止一层:

640?wx_fmt=jpeg

有点意思,接着挖掘。

这会是一个极长的方法,应该对了:

640?wx_fmt=jpeg

方法有点长,复制到一个新 smali 里看看。

为了加快分析我们把出现关键文件夹名称的两个类 StorageUtils 和 DefaultConfigurationFactory 放进去搜索,可以得到这个:

640?wx_fmt=jpeg

初步推断是用私有目录做缓存。

我们来看看这个方法 createDiskCache:

.method public static createDiskCache(Landroid/content/Context;Lcom/nostra13/universalimageloader/cache/disc/naming/FileNameGenerator;JI)Lcom/nostra13/universalimageloader/cache/disc/DiskCache;
.registers 13
invoke-static {p0}, Lcom/nostra13/universalimageloader/core/DefaultConfigurationFactory;->createReserveDiskCacheDir(Landroid/content/Context;)Ljava/io/File;
move-result-object v2
//建立缓存路径,正是uil-images路径出现的原因
const-wide/16 v0, 0x0
cmp-long v0, p2, v0
if-gtz v0, :cond_c
if-lez p4, :cond_1d
.line 83
:cond_c
invoke-static {p0}, Lcom/nostra13/universalimageloader/utils/StorageUtils;->getIndividualCacheDirectory(Landroid/content/Context;)Ljava/io/File;
move-result-object v1
//获取路径准备写入
:try_start_10
new-instance v0, Lcom/nostra13/universalimageloader/cache/disc/impl/ext/LruDiscCache;
move-object v3, p1
move-wide v4, p2
move v6, p4
invoke-direct/range {v0 .. v6}, Lcom/nostra13/universalimageloader/cache/disc/impl/ext/LruDiscCache;->(Ljava/io/File;Ljava/io/File;Lcom/nostra13/universalimageloader/cache/disc/naming/FileNameGenerator;JI)V
:try_end_18
.catch Ljava/io/IOException; {:try_start_10 .. :try_end_18} :catch_19
//初始化缓存模块
:goto_18
return-object v0
.line 87
:catch_19
move-exception v0
.line 88
invoke-static {v0}, Lcom/nostra13/universalimageloader/utils/L;->e(Ljava/lang/Throwable;)V
//异常处理
:cond_1d
invoke-static {p0}, Lcom/nostra13/universalimageloader/utils/StorageUtils;->getCacheDirectory(Landroid/content/Context;)Ljava/io/File;
move-result-object v1
//获取缓存路径准备写入
new-instance v0, Lcom/nostra13/universalimageloader/cache/disc/impl/UnlimitedDiscCache;
invoke-direct {v0, v1, v2, p1}, Lcom/nostra13/universalimageloader/cache/disc/impl/UnlimitedDiscCache;->(Ljava/io/File;Ljava/io/File;Lcom/nostra13/universalimageloader/cache/disc/naming/FileNameGenerator;)V
//初始化缓存限制模块
goto :goto_18
.end method

至此为止,结论出来了。

这个开源库用私有目录作为缓存目录,把展示的图片保存在缓存目录下(虽然我不认为这样可以加速加载)。

然后京东金融 App 显示截图的时候调用了开源库,截图就出现在私有目录下了。

并没有恶意代码,挺正常的对吧?

但真的正常吗?京东金融 App 为何要显示用户的截图?

对于这个问题,开源库作者显然不能背锅。还请京东金融 App 程序员站出来,指使程序员做出这种 feature 的产品经理也难咎其责。


640?wx_fmt=png

京东金融回应:绝对不会收集未经用户授权的任何信息,更不会窃取!


从上我们可以看出,在经过知乎作者@琴梨梨(https://zhuanlan.zhihu.com/p/56875556)一轮代码反编译之后,确实并有没发现京东金融 App 上传用户截图及照片至服务器端的代码,那为何其又要收集用户的截图及照片,并将其保存至 App 的私有目录下?

对此,京东金融客服于昨天下午发布声明表示,之所以会有图片缓存,是因为和此前京东金融 App 中上线的一个名为“图片助手”的便利小功能有关。

640?wx_fmt=jpeg

此外,其还表示,缓存图片只存储在用户本地手机设备内,不会上传到京东金融后台。目前,京东金融 App 已暂时下线“图片助手”小功能,待进一步改进用户安全体验后再上线。


640?wx_fmt=png

风口浪尖的京东金融


然而对着这样一则回应声明,不少网友们并不买账。主要原因有二。

其一:原爆料微博博主@瘦出的肋骨已经消失的大侠阿木 使用的是 Android 手机进行测试,而京东金融给出的回应声明中却是 iPhone 手机测试截图。虽然在完了两个小时之后,京东金融客服声称:“小编刚才一着急,拿自己的苹果手机就截了。这是同事安卓手机的截图。”

640?wx_fmt=png

但通过对比,有网友表示,原声明中苹果手机的截图是 12:07,2个小时之后,补的安卓手机测试截图是 12:04,难道是穿越了?

对于这个问题,如果是京东金融工程师同时用 Android 和 iPhone 对 App 进行了测试,最终运营人员用错了图片,这倒是也可以理解。

但是对于整个声明中,京东金融官方只字未提其 App 私有目录获取照片一事,不少用户也颇为不解。

640?wx_fmt=png

对此,就在刚刚,京东金融再次发微博表示,Android 系统上京东金融 App 5.0.5 之后的版本存在上述问题,当前已下线修复。此外会于下周一即明天邀请权威官方机构对京东金融 App 进行全面安全性检测,邀请包括@瘦出的肋骨已经消失的大侠阿木  在内的用户和外部专家、媒体组成信息安全顾问小组,对其服务进行监督。

640?wx_fmt=png


640?wx_fmt=png

道不清的用户隐私,需要时刻警惕的是我们自己


本想为用户提供更为便捷的服务,万万没想到成为了疑似窃取隐私的元凶。事实上,对于任何一款产品,提出质疑未必不是一件好事,毕竟提出问题才会有反思与改进。

其实,这样的类似事件也并非第一次出现。去年,Facebook 深陷“数据泄露门”之中、QQ 浏览器涉嫌私自调动摄像头、百度手机输入法被指擅自调用录音权限、WiFi 万能钥匙被爆窃取 9 亿用户隐私,甚至于几天前,深圳一家 AI 公司被爆出,人脸数据遭到泄露,导致 256 万用户敏感数据惨遭“裸奔”。

如今随着《网络安全法》的日益完善,虽然现状已经有了良好的改善,但是如果用户自身不时刻保持警惕,或许随手点击的一个“同意”,泄露的可能就是自己全部的家底。

参考来源:

  • https://zhuanlan.zhihu.com/p/56875556;

  • https://zhuanlan.zhihu.com/p/56877625,作者:@琴梨梨,本文已获授权,如需转载,请联系原作者。

【完】

640?wx_fmt=jpeg

 热 文 推 荐 

极客头条

都有 Python 了,还要什么编译器!

技术头条

☞ 35 岁程序员,年后第一天被辞退

☞ 云漫圈 | 学Python还是Java, 8张漫画带你全面分析

☞ 一次性掌握机器学习基础知识脉络 | 公开课笔记

☞ 骗局翻新, 暗网活跃度倍增, 2018加密货币犯罪报告敢看吗?

☞ 程序员年后离职跳槽指南


 

print_r('点个好看吧!');
var_dump('点个好看吧!');
NSLog(@"点个好看吧!");
System.out.println("点个好看吧!");
console.log("点个好看吧!");
print("点个好看吧!");
printf("点个好看吧!\n");
cout << "点个好看吧!" << endl;
Console.WriteLine("点个好看吧!");
fmt.Println("点个好看吧!");
Response.Write("点个好看吧!");
alert("点个好看吧!")
echo "点个好看吧!"

640?wx_fmt=gif点击“阅读原文”,打开 优快云 App 阅读更贴心!

640?wx_fmt=png 喜欢就点击“好看”吧!
<think>嗯,用户想要一份关于.NET Core开发避坑指南及深入学习资料。 首先需要梳理用户的核心需求:既要避开常见陷阱,又要深入学习的系统性资料。根据引用内容,用户提到的资料涉及ASP.NET Core学习资源、.NET生态概览、技术社区建设等,说明用户需要权威资料和实战经验总结。 考虑从三个层面构建回答: 1. 避坑指南部分采用结构化分类,比如配置管理、依赖注入、异步编程等高频问题区,每个点配具体示例和解决方案。 2. 深入学习路径按层级设计,从官方文档到源码阅读逐步深入,特别强调引用[4]提到的DotNetGuide知识库这类中文社区资源。 3. 补充学习建议时重点突出引用[3]和[5]提到的持续学习必要性,以及.NET版本迭代特性。 技术细节方面特别注意: - 配置系统陷阱要明确环境变量优先级问题 - EF Core的跟踪机制需要标注性能影响 - 异步编程强调ConfigureAwait(false)的使用场景 - 跨平台问题需区分Linux和Windows文件系统差异 资源推荐部分严格筛选: - 书籍类优先选择有中文版且评分高的 - 视频课程标注免费资源 - 社区推荐包含但止于引用[4]的DotNetGuide - 实战项目需带GitHub星标数参考 最后生成的问题要引导延伸学习,比如性能优化方向、容器化实践等实用场景。 </think>以下是根据您需求整理的 **.NET Core 开发避坑指南及深入学习资料**,结合权威资源和实战经验,帮助开发者高效掌握核心技术: --- ### **一、避坑指南(高频陷阱及解决方案)** 1. **配置管理陷阱** - **问题**:环境变量、`appsettings.json`、命令行参数优先级混淆导致配置错误。 - **方案**: - 明确优先级顺序:命令行参数 > 环境变量 > `appsettings.{Environment}.json` > `appsettings.json`[^5] 。 - 使用 `IConfiguration` 统一读取: ```csharp var connectionString = _config.GetConnectionString("Default"); ``` 2. **依赖注入(DI)误用** - **问题**: - 服务生命周期(Singleton/Scoped/Transient)选择当引发内存泄漏或数据竞争。 - 在构造函数中直接解析 `Scoped` 服务(导致 `Captive Dependency`)。 - **方案**: - **Singleton**:无状态服务;**Scoped**:数据库上下文(如 EF Core 的 `DbContext`);**Transient**:轻量级临时服务[^1][^5] 。 - 避免在构造函数注入 `Scoped` 服务,改用方法注入(`[FromServices]`)。 3. **异步编程死锁** - **问题**:错误使用 `.Result` 或 `.Wait()` 导致线程阻塞。 - **方案**: - 始终使用 `async/await` 链式调用,禁止混用同步阻塞方法。 - 在库代码中调用 `ConfigureAwait(false)` 避免上下文捕获: ```csharp var data = await httpClient.GetAsync(url).ConfigureAwait(false); ``` 4. **EF Core 性能问题** - **问题**: - `N+1 查询`(循环中访问导航属性)。 - 未关闭跟踪(`AsNoTracking()`)导致内存激增。 - **方案**: - 使用 `.Include()` 预加载关联数据,或通过 `Select` 投影所需字段。 - 只读查询必加 `AsNoTracking()`: ```csharp var users = dbContext.Users.AsNoTracking().Where(u => u.IsActive).ToList(); ``` 5. **跨平台路径问题** - **问题**:硬编码 `\` 路径分隔符导致 Linux 部署失败。 - **方案**: - 使用 `Path.Combine()` 构建路径: ```csharp var filePath = Path.Combine(Directory.GetCurrentDirectory(), "data", "config.json"); ``` --- ### **二、深入学习资料推荐** #### **1. 官方核心资源** - **文档**: - [.NET 官方文档](https://learn.microsoft.com/dotnet/core/)(更新快,含实战样例)[^1][^5] 。 - [ASP.NET Core 性能优化指南](https://learn.microsoft.com/aspnet/core/performance)(缓存、响应压缩等)[^1] 。 - **GitHub 源码**: - [.NET Runtime 源码](https://github.com/dotnet/runtime)(理解底层机制如 DI、配置系统)[^4] 。 #### **2. 中文社区精选** - **DotNetGuide 知识库**: - 开源项目地址:[DotNetGuide on GitHub](https://github.com/YSGStudyHards/DotNetGuide) - 内容:学习路线图、面试题集、框架对比(如 EF Core vs Dapper)、容器化部署指南[^4] 。 - **书籍**: - 《ASP.NET Core 高并发实战》 - 深入中间件、分布式架构设计。 - 《深入理解C#》 - 语言特性与异步编程本质[^1][^4] 。 #### **3. 实战进阶** - **视频教程**: - B站系列:[.NET Core 微服务实战 by 微软 MVP](https://www.bilibili.com/video/BV1q64y1u7Jk)(含鉴权、消息队列集成)。 - **开源项目参考**: - [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers):微服务架构范例(Docker+K8s)。 - [OrchardCore](https://github.com/OrchardCMS/OrchardCore):模块化 CMS 系统(学习插件化设计)[^5] 。 #### **4. 扩展技能** | 领域 | 推荐工具/框架 | 学习重点 | |--------------|------------------------|-----------------------------| | 性能监控 | Application Insights | 请求追踪、异常聚合 | | 单元测试 | xUnit + Moq | 模拟依赖、集成测试策略 | | 云原生部署 | Docker + Kubernetes | 健康检查、水平扩缩容配置 | --- ### **三、关键学习建议** 1. **版本选择**:优先使用 LTS 版本(如 .NET 6/8),避免非稳定版带来的兼容性问题[^5] 。 2. **社区参与**: - 关注 .NET Conf 年度技术大会视频([官网存档](https://dotnetconf.net/))。 - 加入 [CNCF .NET 工作组](https://www.cncf.io/) 获取云原生实践案例[^3] 。 3. **调试技巧**:善用 `dotnet-counters` 和 `dotnet-trace` 命令行工具诊断内存泄漏与 CPU 峰值[^1] 。 > 引用资源备注: > [^1]: ASP.NET Core 资源指南(配置/性能专题) > [^4]: DotNetGuide 开源知识库(中文社区精华) > [^5]: .NET 生态演进与工具链详解 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值