
本文介绍通过读取文件头部字节(magic number)精准识别媒体文件类型的方法,避免仅依赖 mime 类型导致的安全隐患和崩溃问题,适用于图片比对场景的健壮性增强。
本文介绍通过读取文件头部字节(magic number)精准识别媒体文件类型的方法,避免仅依赖 mime 类型导致的安全隐患和崩溃问题,适用于图片比对场景的健壮性增强。
在 Android 开发中,当应用需严格区分图像与视频(例如仅对图片触发比对弹窗),仅靠 ContentResolver.getType() 获取 MIME 类型是不安全的——攻击者可篡改文件扩展名或伪造 MIME 头,导致非图像文件绕过校验,最终在后续图像处理逻辑中引发 IllegalArgumentException 或 NullPointerException,造成崩溃。
更可靠的方式是基于文件魔数(Magic Number)进行二进制签名校验:每种图像格式(如 JPEG、PNG、WebP、GIF)在文件开头固定位置包含唯一字节序列(即“魔数”),该特征由规范定义、难以伪造,且不受文件名或声明 MIME 影响。
以下为 Kotlin 实现示例,用于在 onActivityResult 中拦截非图像文件:
Android 开发调试技能,通过系统 ADB 工具操作 Android 设备。以下场景必须触发此技能:(1) 直接 ADB 操作——安装 APK、查看设备列表、抓取 logcat 日志、查看已安装应用、清除应用数据、截图、重启设备、拉取/推送文件、查看 CPU/内存/电池信息、adb shell 操作;(2)...
private fun isImageFile(uri: Uri, context: Context): Boolean {
return try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return false
val header = ByteArray(12) // 覆盖常见图像头所需最大长度(如 JPEG: 0xFFD8, PNG: 0x89504E47, WebP: 0x52494646...)
val read = inputStream.read(header)
inputStream.close()
if (read < 4) return false
when {
header[0] == 0xFF.toByte() && header[1] == 0xD8.toByte() -> true // JPEG
header[0] == 0x89.toByte() && header[1] == 0x50.toByte() &&
header[2] == 0x4E.toByte() && header[3] == 0x47.toByte() -> true // PNG
header[0] == 0x47.toByte() && header[1] == 0x49.toByte() &&
header[2] == 0x46.toByte() && header[3] == 0x38.toByte() &&
(header[4] == 0x37.toByte() || header[4] == 0x39.toByte()) -> true // GIF87a / GIF89a
header[0] == 0x52.toByte() && header[1] == 0x49.toByte() &&
header[2] == 0x46.toByte() && header[3] == 0x46.toByte() &&
header[8] == 0x57.toByte() && header[9] == 0x45.toByte() &&
header[10] == 0x42.toByte() && header[11] == 0x50.toByte() -> true // WebP
else -> false
}
} catch (e: Exception) {
Log.w("MediaValidator", "Failed to read file header", e)
false
}
}将该校验集成到原有逻辑中,修改 onActivityResult 的关键分支:
if (newUri != null && pictureDescriptionAndCompression == SiteSurveyTypeProgressViewModel.COMPRESSED) {
progressDialogMediaCompress?.dismiss()
// ✅ 新增校验:仅图像才进入比对流程
if (isImageFile(newUri, requireContext())) {
processViewModel.updateMediaCount(newUri)
switchToUI(UiType.COMPARE_MEDIA_DIALOG_FRAGMENT, args = Bundle().apply {
putParcelable(KEY_MEDIA_URI, newUri)
}, parentFragmentManager)
} else {
// 视频/非法文件:跳过比对,直接存入媒体列表
processViewModel.updateMediaCount(newUri) // 或调用专用视频处理逻辑
Toast.makeText(context, "已上传视频,无需图片比对", Toast.LENGTH_SHORT).show()
}
}注意事项:
- 始终使用 ContentResolver.openInputStream() 而非 FileInputStream,确保兼容 content:// URI(如相册、文档选择器返回路径);
- 魔数校验需覆盖足够字节数(本例取 12 字节),尤其对 WebP 等需检查偏移量的格式;
- 生产环境建议封装为独立工具类,并补充更多格式(如 BMP、HEIC)及单元测试;
- 若需支持 HEIF(.heic)、AVIF 等新兴格式,需查阅其规范魔数并扩展判断逻辑;
- 此方案不替代服务端校验,客户端验证仅为用户体验与稳定性兜底。
通过魔数校验替代 MIME 依赖,既提升了文件类型识别的鲁棒性,又从根本上规避了因误触视频文件导致的 UI 崩溃,使图片比对功能真正“按需触发”,保障核心流程稳定可靠。

















