插件认证与安全
插件运行在用户的工作环境里,能读取文件、执行命令、发起网络请求。扩展市场的信任建立在安全之上:Marketplace 会对插件做审查,开发者也要从设计上遵守最小权限原则。
activationEvents 最小权限原则
激活事件决定生命周期
插件只有在满足激活条件时才被加载,加载时机由 package.json 的 activationEvents 声明:
{
"activationEvents": [
"onCommand:myext.format",
"onLanguage:json"
]
}滥用激活事件最常见的后果:插件在 VS Code 启动时就被加载,长期占用内存,还扩大了攻击面。
常见激活事件
| 事件 | 触发时机 | 适用场景 |
|---|---|---|
onCommand:命令ID | 用户执行命令时 | 命令式功能 |
onLanguage:json | 打开对应语言文件时 | 语言相关功能 |
onView:视图ID | 打开侧边栏视图时 | 自定义视图 |
onStartupFinished | 编辑器启动完成后 | 后台任务 |
* | 编辑器启动即激活 | 极少使用 |
禁止无差别常驻
"activationEvents": ["*"] 会让插件在 VS Code 每次启动时都激活,即使完全没被使用。除非插件需要监听全局状态(如状态栏常驻显示),否则不要使用。
从 v1.74 起激活事件可省略
VS Code 从 1.74 版本开始,只要插件在 package.json 中声明了 main 字段,就可以不写 activationEvents——编译器会根据 contributes 中的命令、语言等自动生成激活事件。这种声明式写法配合 exports API 机制,实现按需加载:
{
"main": "./dist/extension.js",
"engines": {
"vscode": "^1.85.0"
}
}导出 API 实现按需激活
插件通过 exports 暴露 API,消费者实际调用时才触发加载:
// 插件侧:导出 API
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 初始化逻辑
}
export function getFormatter() {
// 按需暴露的能力,不随激活执行
return { format: (text: string) => text.trim() };
}// 消费者侧:实际调用时才激活本插件
const mod = await vscode.extensions.getExtension('mypublisher.my-extension')
?.activate();
const formatter = mod?.getFormatter();最小权限自查清单
发布前对照检查:
- 不使用的命令没有对应的
onCommand事件 - 没有
*激活事件 - 监听的文件类型与实际处理的一致
- 启动时的后台任务有明确的结束条件
声明式权限与安全审查
Marketplace 的权限说明
Marketplace 要求插件明确说明所需权限。插件页面会展示权限清单,用户安装时可见。activationEvents 中声明的事件越多,权限说明越长,用户信任度越低。
权限最小化实践
| 权限 | 最小化做法 |
|---|---|
| 文件读取 | 只读 workspace.workspaceFolders 中的文件 |
| 网络请求 | 只请求功能必需的域名 |
| 命令执行 | 不通过 vscode.commands.executeCommand 执行无关命令 |
| 环境变量 | 不读取敏感环境变量 |
避免执行任意代码
插件可以 require 任意模块,这是一把双刃剑。以下行为会在安全审查中亮红灯:
- 从网络动态下载并执行脚本
eval用户输入或配置文件内容- 读取并外发用户的密钥文件
网络请求安全
HTTPS 全覆盖
插件所有网络请求必须走 HTTPS,明文 HTTP 会被 Marketplace 标记并可能被中间人篡改:
// 正确:HTTPS + 超时 + 错误处理
import * as https from 'https';
export function fetchRemoteConfig(): Promise<string> {
return new Promise((resolve, reject) => {
const req = https.get(
{
hostname: 'api.example.com',
path: '/config',
timeout: 5000
},
(res) => {
if (res.statusCode !== 200) {
reject(new Error('HTTP ' + res.statusCode));
return;
}
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => resolve(body));
}
);
req.on('timeout', () => req.destroy(new Error('请求超时')));
req.on('error', reject);
});
}Token 用 SecretStorage 存储
插件需要的 API Token 绝不能明文写进配置文件或 settings.json。ExtensionContext.secrets 提供加密存储:
import * as vscode from 'vscode';
export async function activate(context: vscode.ExtensionContext) {
// 保存令牌:写入系统密钥链,非明文
await context.secrets.store('myext.apiToken', 'sk-xxxxx');
// 读取令牌
const token = await context.secrets.get('myext.apiToken');
// 删除令牌
await context.secrets.delete('myext.apiToken');
}| 存储方式 | 安全性 | 说明 |
|---|---|---|
settings.json | 低 | 明文写入用户配置文件,还会被同步 |
context.globalState | 中 | 明文存于插件目录,非密钥链 |
context.secrets | 高 | 系统密钥链加密存储 |
请求敏感接口的注意事项
- 发送 Token 时使用
Authorization请求头,不要拼进 URL - 记录日志时脱敏,不打印完整令牌
- 令牌泄露后提供"重新输入"的入口
代码签名需求
VS Code 1.86 及以上版本对 Marketplace 上的插件实施强制代码签名。发布流程自动完成:vsce publish 上传后,Marketplace 对 .vsix 做签名验证,未签名或签名校验失败的扩展无法安装。
开发者需要注意:
- 由 vsce 正常发布的插件自动获得签名
- 从第三方来源手动安装的
.vsix可能被拒绝(--force也无法绕过签名检查) - 依赖篡改的插件在加载时会报错并被隔离
这要求开发环境与 CI 环境都保持 vsce 版本较新,旧版 vsce 打包的产物可能无法通过新签名校验。
extensionKind 与运行模式
extensionKind 的三种模式
extensionKind 声明插件运行的位置:
{
"extensionKind": ["workspace", "ui"]
}| 值 | 运行位置 | 适用场景 |
|---|---|---|
workspace | 与工作区相同(本地或远程) | 需要访问工作区文件、语言服务器 |
ui | 始终在本地 UI 进程 | 仅操作编辑器 UI |
workspace + ui | 双实例 | 同时需要两者 |
remote 环境适配
用户通过 Remote-SSH、Remote-Containers 等方式连接远程环境时:
ui扩展:运行在本地窗口,无法直接访问远程文件系统workspace扩展:随远程环境运行,可访问远程文件
需要访问远程文件的能力(如格式化、lint)应声明为 workspace:
{
"extensionKind": ["workspace"]
}多实例协作
同时声明 workspace 与 ui 时,插件会在本地与远程各启动一个实例。两个实例通过 workspace.onDidChangeConfiguration 等 API 感知彼此存在,避免重复执行初始化。
检测运行环境
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 判断当前是否在远程/容器环境
const remoteName = vscode.env.remoteName; // undefined 表示本地
if (remoteName === 'ssh-remote') {
// 远程 SSH 环境:使用远程路径
} else if (remoteName === 'dev-container') {
// 容器环境
} else {
// 本地环境
}
}安全设计清单
发布前对照下面的检查项逐条核对:
| 检查项 | 要求 |
|---|---|
| 激活事件 | 只声明必需的,无 * 常驻 |
| 网络请求 | 全部 HTTPS,含超时与错误处理 |
| 令牌存储 | 使用 context.secrets,不落明文 |
| 日志 | 不打印 Token、密钥等敏感信息 |
| 文件权限 | 只读写功能必需的文件 |
| 命令执行 | 不执行来源不可信的命令 |
| 依赖来源 | 依赖版本锁定,关注安全通告 |
| extensionKind | 远程场景声明正确运行模式 |
安全不是发布后的补丁,而是设计阶段的约束:从激活事件到网络请求,从令牌存储到运行模式,每个决定都在为用户的数据安全投票。