插件安全性审查指南
Obsidian 插件生态的开放性是其核心优势,但也带来了安全风险。本文为插件开发者提供安全审查清单和最佳实践,帮助确保插件安全可靠。
为什么插件安全很重要
Obsidian 插件拥有对本地文件系统的完全访问权限,这意味着:
- 可以读取、修改、删除你的所有笔记
- 可以访问网络(发送数据到外部服务器)
- 可以执行任意代码
- 可以访问剪贴板内容
安全提醒
Obsidian 官方对社区插件进行代码审查,但无法保证 100% 安全。用户应自行评估插件的可信度。
安全审查清单
1. 网络请求
检查项
- [ ] 所有网络请求是否必要?
- [ ] 是否向第三方服务器发送用户数据?
- [ ] 网络请求是否使用 HTTPS?
- [ ] 是否在设置中明确告知用户数据发送行为?
- [ ] 请求频率是否合理?
最佳实践
typescript
// ❌ 不好的做法:静默发送用户数据
async function sendData(content: string) {
await requestUrl({
url: 'https://example.com/api',
method: 'POST',
body: JSON.stringify({ content })
});
}
// ✅ 好的做法:告知用户并获得同意
async function sendData(content: string) {
if (!this.settings.allowDataSending) {
new Notice('请在设置中启用数据发送功能');
return;
}
await requestUrl({
url: 'https://example.com/api',
method: 'POST',
body: JSON.stringify({ content }),
headers: { 'Content-Type': 'application/json' }
});
}2. 文件系统操作
检查项
- [ ] 文件读取是否限制在必要范围?
- [ ] 文件写入是否有备份机制?
- [ ] 文件删除是否有确认提示?
- [ ] 路径处理是否防止目录遍历攻击?
- [ ] 是否正确处理文件名中的特殊字符?
最佳实践
typescript
// ❌ 不好的做法:直接使用用户输入作为路径
const path = `${vaultPath}/${userInput}`;
// ✅ 好的做法:验证和规范化路径
import { normalizePath } from 'obsidian';
function safePath(userInput: string): string {
const normalized = normalizePath(userInput);
// 防止目录遍历
if (normalized.startsWith('..') || normalized.startsWith('/')) {
throw new Error('Invalid path');
}
return normalized;
}3. 代码执行
检查项
- [ ] 是否使用了
eval()或类似动态代码执行? - [ ] 是否使用了
child_process执行系统命令? - [ ] 是否加载了外部脚本?
- [ ] 动态代码执行是否可被用户输入注入?
最佳实践
typescript
// ❌ 不好的做法:使用 eval 执行动态代码
const result = eval(userExpression);
// ✅ 好的做法:使用安全的替代方案
// 如果需要数学表达式,使用专门的数学解析库
import { evaluate } from 'mathjs';
const result = evaluate(userExpression);4. 数据存储
检查项
- [ ] 敏感数据(API Key、密码)是否加密存储?
- [ ] 插件数据是否保存在
.obsidian/plugins/目录中? - [ ] 是否避免在笔记中明文存储凭据?
- [ ] 数据格式是否使用标准 JSON?
最佳实践
typescript
// ❌ 不好的做法:明文存储 API Key
this.settings.apiKey = 'sk-xxxxxxxx';
// ✅ 好的做法:使用 Obsidian 的安全存储
import { loadLocalStorage, saveLocalStorage } from 'obsidian';
// 存储敏感信息
saveLocalStorage('my-plugin-api-key', apiKey);
// 读取敏感信息
const apiKey = loadLocalStorage('my-plugin-api-key');5. 依赖安全
检查项
- [ ] 依赖包是否来自可信来源?
- [ ] 依赖包数量是否最小化?
- [ ] 是否定期更新依赖修复安全漏洞?
- [ ] 是否使用
package-lock.json锁定版本?
最佳实践
json
// package.json — 最小化依赖
{
"dependencies": {
// 只保留必要的依赖
// 避免引入大型工具库(如 lodash)如果只用其中一两个函数
}
}6. 隐私保护
检查项
- [ ] 是否收集用户使用数据?
- [ ] 隐私政策是否清晰说明数据使用?
- [ ] 用户是否可以选择退出数据收集?
- [ ] 是否遵循 GDPR 等数据保护法规?
安全发布流程
发布前检查
- 运行安全审查清单
- 确保所有设置项有合理的默认值
- 编写安全相关的 README 说明
- 测试极端输入和边界情况
README 安全说明模板
markdown
## 安全说明
### 数据处理
- 本插件 [不/会] 发送数据到外部服务器
- 发送的数据包括:[列出数据类型]
- 数据发送目的地:[列出服务器]
### 权限需求
- 文件读取:[说明原因]
- 文件写入:[说明原因]
- 网络访问:[说明原因]
### 隐私
- 本插件 [不/会] 收集使用数据
- 敏感信息存储方式:[说明]版本更新安全
- 在 CHANGELOG 中记录安全修复
- 对破坏性变更提供迁移指南
- 不自动移除安全相关设置
- 使用语义化版本号标记安全修复
常见安全问题
1. XSS 攻击
typescript
// ❌ 不好的做法:直接插入 HTML
containerEl.innerHTML = userInput;
// ✅ 好的做法:使用 DOM API 或转义
containerEl.createEl('span', { text: userInput });
// 或使用转义函数
function escapeHtml(str: string): string {
return str.replace(/[&<>"']/g, c => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
})[c]);
}2. 正则表达式拒绝服务(ReDoS)
typescript
// ❌ 不好的做法:可能导致 ReDoS 的正则
const regex = /(a+)+b/;
// ✅ 好的做法:简化正则表达式
const regex = /a+b/;3. 原型污染
typescript
// ❌ 不好的做法:深度合并可能导致原型污染
function deepMerge(target: any, source: any) {
for (const key in source) {
target[key] = typeof source[key] === 'object'
? deepMerge(target[key] || {}, source[key])
: source[key];
}
return target;
}
// ✅ 好的做法:使用 Object.create(null) 和白名单
function safeMerge(target: any, source: any, allowedKeys: string[]) {
for (const key of allowedKeys) {
if (key in source) {
target[key] = source[key];
}
}
return target;
}用户端安全建议
作为插件使用者,也应关注安全问题:
- 只安装必要的插件 — 每个插件都是潜在的安全风险
- 检查插件源码 — 特别是涉及网络请求和文件操作的插件
- 关注插件更新 — 及时更新以获取安全修复
- 定期审查已安装插件 — 移除不再使用的插件
- 备份重要数据 — 在安装新插件前备份仓库