安全最佳实践:SQL 注入防护、路径遍历防护、数据加密
说实话,Content Provider 的安全问题,我见过太多人栽跟头了。我自己早期做项目时,也踩过几个坑。今天咱们就把这三个最要命的安全问题——SQL 注入、路径遍历、数据加密——彻底聊透。
一、SQL 注入防护:别让查询变成灾难
先问个问题:你的 Content Provider 是怎么处理外部传入的查询条件的?
很多人习惯直接拼接字符串:
// 危险!千万别这么写
String selection = "name = '" + userInput + "'";
Cursor cursor = db.query("users", null, selection, null, null, null, null);
嗯,这就是典型的 SQL 注入漏洞。用户输入 ' OR 1=1 --,你的整个表就裸奔了。
正确的做法是什么? 用参数化查询:
// 安全写法
String selection = "name = ?";
String[] selectionArgs = new String[]{userInput};
Cursor cursor = db.query("users", null, selection, selectionArgs, null, null, null);
我个人习惯,只要涉及外部输入,一律用 ? 占位符。哪怕你觉得这个输入“肯定安全”,也别偷懒。我曾经在一个项目中,就因为觉得“用户 ID 是数字,不会有问题”,直接拼了字符串。结果 QA 同学传了个 1; DROP TABLE users; --,测试库直接崩了。嗯,从那以后我再也不敢了。
selectionArgs,也要注意 LIKE 查询中的 % 和 _ 通配符。用户输入中如果包含这些字符,可能会匹配到意料之外的数据。建议对用户输入做转义处理。
二、路径遍历防护:别让文件暴露在外
如果你的 Content Provider 提供了文件访问功能(比如通过 openFile() 或 openAssetFile()),那路径遍历就是你必须面对的敌人。
什么叫路径遍历?说白了,就是用户传个 ../../data/data/com.example/databases/secret.db,想绕过你的限制去读不该读的文件。
我见过最典型的错误写法:
// 危险!直接拼接路径
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String path = uri.getLastPathSegment();
File file = new File(getContext().getFilesDir(), path);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
用户传个 content://com.example.provider/../../sdcard/secret.txt,你的 getLastPathSegment() 拿到的是 ../../sdcard/secret.txt,然后直接拼到 getFilesDir() 后面……后果你自己想。
怎么防? 两步走:
- 规范化路径:用
File.getCanonicalPath()把路径转成标准形式。 - 检查前缀:确保最终路径在你允许的目录下。
// 安全写法
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String path = uri.getLastPathSegment();
File targetFile = new File(getContext().getFilesDir(), path);
try {
String canonicalPath = targetFile.getCanonicalPath();
String allowedDir = getContext().getFilesDir().getCanonicalPath();
// 检查路径是否在允许的目录下
if (!canonicalPath.startsWith(allowedDir)) {
throw new SecurityException("访问被拒绝");
}
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
} catch (IOException e) {
throw new FileNotFoundException("文件不存在");
}
}
UriMatcher 做白名单匹配。只允许访问你明确列出的文件,其他一概拒绝。这样最省心。
三、数据加密:敏感数据必须上锁
Content Provider 存储的数据,很多都是敏感信息——用户密码、支付信息、个人资料。这些数据如果明文存储,一旦设备被 root 或者备份文件被窃取,后果不堪设想。
我个人建议,敏感数据在写入 Content Provider 之前就应该加密。不要指望 Content Provider 本身来做加密,因为加密密钥的管理是个大问题。
Android 提供了 EncryptedSharedPreferences 和 Android Keystore,但如果你用的是 SQLite 数据库,我推荐用 SQLCipher:
// 使用 SQLCipher 加密数据库
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(
databaseFile,
"your_encryption_key",
null
);
当然,密钥不能硬编码在代码里。我习惯把密钥存在 Android Keystore 中,只有应用自身能读取:
// 生成并存储密钥到 Keystore
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
);
keyGenerator.init(
new KeyGenParameterSpec.Builder("my_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
);
SecretKey key = keyGenerator.generateKey();
这里有个坑:密钥轮换。我曾经在一个金融类项目中,密钥用了两年没换。后来安全审计时被要求整改。建议定期更换密钥,并且旧密钥要妥善销毁。
知识体系总览
下面这张图,把今天讲的三个安全维度和它们的关系梳理了一下:
总结一下
今天聊的三个安全点,其实核心就一句话:永远不要信任外部输入。不管是 SQL 查询参数、文件路径,还是存储的数据,都要假设它们可能被恶意利用。
我个人的经验是,安全防护要尽早做。等项目上线了再回头补安全,成本高不说,还容易漏。你想想看,一个 SQL 注入漏洞,可能从你写第一行代码时就埋下了。等到用户数据泄露了再追责,那就晚了。
嗯,今天就聊到这儿。记住:安全无小事,细节定成败。