---
name: v8-security
description: Microi V8 安全指南。用于审查 DiyToken 与权限、可逆业务秘密、Passkey/TOTP/人脸步进验证、接口引擎安全、密钥管理、SQL 注入、匿名端点、文件上传和租户隔离。
---

> **Codex 非阻塞自动更新：** 当前宿主为 Codex 时，吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新；需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。

# Microi V8 安全最佳实践

你正在开发 Microi 吾码平台的 V8 引擎代码，必须遵守以下安全规范。

访问密钥由 `microi_list_my_access_keys`、`microi_create_my_access_key`、`microi_revoke_my_access_key` 管理，只允许当前用户、限期、最小 scope，明文仅创建时返回一次。外部身份回调固定为 `/api/ExternalLogin/Callback`，服务端校验租户、Provider、state、redirect 和回调域名，验证成功后仍签发 DiyToken。

官方升级资源属于独立控制面。`get-microi-upgrade-resource` 可以匿名读取固定白名单，但 `Publish/PublishBatch` 必须调用仅绑定该 Managed ApiEngineKey 的 `V8.Method.AuthorizeOfficialResourcePublish()`：固定 `iTdos` 官方租户、拒绝访问密钥会话，并从主库复核当前用户、状态和平台管理员角色。禁止只相信 `V8.CurrentUser.Level`，也禁止把这个可信原子复用于普通接口、租户 Hook 或表单事件；资源校验、SHA 乐观锁、事务行锁、写入及回读仍由 Managed V8 编排。

<!-- microi-progressive:begin -->
<!-- microi-progressive:chunk id=v8-security-000 sha256=2e81552d91b61d9d7f96cba1502306e186f0a288d5db8d443c4e524fce748bc9 -->
## 0. 租户动态系统设置与密钥边界

**AI 默认处理路径：** 用户交付第三方 Secret 并授权配置时，优先调用 `microi_manage_server_private_secret`：List 只确认 Key/HasSecret；Save 使用 `confirmExecution=SAVE:<ConfigKey>`，固定 `IsSecret=true / IsPublic=false` 并自动脱敏回读。例如畅捷通使用 `Integration.Changjet.AppSecret`，后端从 `V8.SysConfig.ServerPrivateSettings` 使用。不要仅拒绝硬编码后停止，也不要因为 `OsClientModel` 不暴露密钥而误判服务端 V8 无法调用第三方。工具不提供原文揭示；更换已有 Key 前回读现状，超时只回读、不盲目重写。

第三方密钥（微信、支付宝、OpenAI、阿里云、ERP、SMTP）**禁止**硬编码在 V8 代码或前端。公开的租户配置必须建成当前租户 `sys_config` 的实体字段；敏感或仅供后端使用的租户业务配置保存到 `mci_system_setting`。数据库、Redis、MongoDB、MinIO、MQ 等部署控制面仍由主库 `sys_osclients` 托管，子租户不能修改。

能力是否启用、入口是否显示、公开交互模式等 Bool/Enum 配置即使属于登录或第三方集成，也必须放在 `sys_config`；API Key、ClientSecret、RP ID、Origin、Issuer、Scope、供应商地址等后端参数才放在 `mci_system_setting`。禁止给同一个新配置双写两张表。迁移旧开关时采用“新 `sys_config` 显式值 → 旧私密 Key → 存量安全默认”的只读回退，并从私密设置的列表、保存和删除入口移除旧 Key。

```javascript
// ✅ 浏览器/前端 V8 只读取 sys_config 的浏览器安全投影
var sysTitle = V8.SysConfig.SysTitle;
var githubVisible = V8.SysConfig.DisableLoginGitHub !== 1;

// ✅ 后端接口引擎/后端 V8 事件从独立节点读取私密设置
var privateSettings = V8.SysConfig.ServerPrivateSettings || {};
var clientSecret = privateSettings['Login.Gitee.ClientSecret'];
// 只能在后端使用，禁止 return、日志、审计或写入前端可读数据。

// ❌ 危险：密钥泄漏 / 跨租户串号
var openaiKey = 'sk-xxxxxxxxxx';
```

`V8.OsClientModel` 与兼容别名 `V8.ClientModel` 均为独立脱敏副本：数据库连接、AuthSecret、Redis、对象存储、MQ、MQTT、Search 的地址与凭据不会注入脚本。存量租户业务字段只作兼容，新增 Secret 不得继续依赖 `V8.OsClientModel`。

接口引擎需要保存可逆的接口私有密码或 Token 时，使用 `V8.Method.ProtectApiEngineSecret(value)` 写入密文，读取时使用 `V8.Method.UnprotectApiEngineSecret(cipher)`。宿主固定绑定当前 `OsClient + ApiEngineKey`，V8 不能指定租户、Purpose 或密钥；同租户其它接口引擎也不能解密。禁止继续用 `V8.OsClientModel.AuthSecret/DbConn` 自行派生 AES 密钥。列表仍只返回脱敏元数据，解密前仍要校验当前用户、行归属和业务权限，原文不得进入日志、审计或匿名响应。

`V8.SysConfig` 按运行端采用不同权限投影，且任何运行端都不存在 `PublicSettings` 属性。浏览器/前端 V8 只得到匿名 `GetSysConfig` 的独立脱敏 `sys_config` 投影；`mci_system_setting` 的任何记录都不会进入浏览器。后端接口引擎和后端 V8 事件得到当前租户完整、独立的 `sys_config`，全部启用的 `mci_system_setting` 则放在 `V8.SysConfig.ServerPrivateSettings`，Secret 由可信后端按租户解密。独立节点避免动态 Key 覆盖 `sys_config` 实体字段，也让前后端边界可审计。子租户调用 `V8.FormEngine.GetSysConfig(...)` 时仍强制使用当前 `OsClient`，不能借缓存命中读取其它租户配置。

Secret 只通过租户管理员专用端点写入租户绑定的认证密文。列表不返回密文或原文；临时显示必须消费 Passkey/TOTP/严格人脸一次性票据，设置 `no-store`，30 秒清除，审计不含原文。前端 V8、普通 FormEngine HTTP、匿名请求和访问密钥会话不得读取 `SecretCipher`、`ServerPrivateSettings` 或 Secret 原文；后端 V8 只能通过当前租户 `V8.SysConfig.ServerPrivateSettings[ConfigKey]` 使用已解密值，不获得通用解密器，也不得返回或记录原文。

共享基础设施只能通过受控能力访问：`V8.Cache` 自动绑定 `Microi:{OsClient}:*`，文件路径绑定 `/{OsClient}/...`，RabbitMQ 队列绑定 `microi.{OsClient}.*`，MQTT Topic 绑定 `tenant/{OsClient}/...`，Search 索引绑定 `{OsClient}_*`。V8 不得获得 Redis `IDatabase`、HDFS `ClientModel` 或原始基础设施配置。

子租户缺少 RabbitMQ/MQTT/Search 独立凭据时必须失败关闭，禁止回退主租户账号。新租户开通只有在外部 broker/search 中真实创建 user、vhost、ACL 或 API Key 后，才能标记对应服务可用。

登录和管理端必须强制 HTTPS。登录 RSA 只用于避免密码在请求体、代理调试界面中直接显示，不能替代 HTTPS，也不能作为身份认证或密码存储密钥。平台为兼容已发布客户、旧前端和浏览器缓存，保留历史登录 RSA 密钥对作为缺省回退；安全修复不得直接删除该回退并造成全量客户无法登录。需要部署专属密钥时，在主租户 SaaS 引擎【后端运行配置】中成对维护 `BackendLoginRsaPrivateKey` 与 `BackendLoginRsaPublicKey`；私钥只允许可信服务端读取，匿名 `GetSysConfig` 只返回匹配公钥。源码、环境变量、普通 V8、前端业务代码和日志中仍禁止新增或输出真正的业务私钥、JWT 密钥、支付密钥及对象存储凭据。

微信内容安全回调固定使用 `/api/wechatcontentsecurity/callback` 或第三方不支持 QueryString 时以 `/api/wechatcontentsecurity/callback--osclient--` 为路径前缀并追加 `{OsClient}--`；回调只能由服务端按签名和租户规则建立信任。

吾码现有多端兼容约定是：主 SaaS 引擎 `sys_osclients.CorsAllowOrigins` 为空时默认允许全部跨域，便于本地开发、独立前端、H5 和不同租户域名访问；配置了来源后才按精确来源或通配符限制。安全修复不得把“未配置”改成默认拒绝，否则会造成所有存量部署和本地调试突然失效。CORS 不是鉴权边界，权限仍必须依赖 Token、租户隔离、菜单/表权限和服务端数据范围。

吾码既有客户大量通过 `V8.Http` 访问内网设备、InfluxDB、内部 ApiEngine 和本机 sidecar。严格 SSRF 防护必须默认关闭：未配置时不得限制协议、URL 内嵌凭据、回环、私网、链路本地、云元数据或重定向。只有客户在 SaaS 引擎主租户启用 `SsrfProtectionEnabled` 后才进入严格模式，并用精确 `SsrfAllowedHosts` 放行；不要为这类普通运行参数增加 API 容器环境变量。

外部数据库与附件迁移属于更高风险的控制面操作：

- `microi_database` 只允许 `Level >= 9999` 的可信管理链路维护；连接字符串、密码和鉴权 Header 不得出现在日志、接口返回、前端或审计详情。
- MCP 临时连接和保存连接只接受平台认证数据库类型；保存前测试写连接和独立读连接，写入必须显式确认，返回只包含 DbKey、类型和回读状态。
- `microi_query_external_database` 是默认只读入口；`microi_execute_external_database` 是独立超级管理员入口，后端必须从当前 Token 硬校验 `Level >= 9999`。显式确认后不限制 SQL 类型，可执行数据库账号有权执行的 DML、DDL、存储过程、文件能力和多语句；审计仅保留 SQL 哈希、长度、模式和结果。
- `V8.Dbs.Open` 只能在可信后端代码中使用，连接串来自服务端密钥或管理员配置；禁止把 `V8.Param`、Header 或匿名请求里的连接串直接传入。
- `microi_import_external_attachment` 仅对后端确认的 `Level >= 9999` 当前用户开放，可访问 HTTP/HTTPS、私网、本机绝对路径和 UNC；不设固定 MCP 大小上限并采用流式迁移。源 URL、鉴权 Header、本机/UNC 路径只以哈希进入审计，能力仍受 API 服务账号和目标基础设施授权约束。
- 多节点保存连接使用按 `OsClient + DbKey` 隔离的分布式锁，并由数据库唯一索引兜底；同步数据和附件仍必须使用业务幂等键，锁不能替代唯一约束、状态机或 inbox/outbox。

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=v8-security-001 sha256=ccec93b880295123923b2375dbc616c5ab02aaf43af9a79f26b7ac953609866d -->
## 0.5 接口引擎配置安全

代码以外，接口本身的配置项也是安全防线（详见 `v8-api-config/SKILL.md`）：

| 配置 | 何时开启 |
|------|---------|
| `IsAnonymous = false` | 非公开接口默认关闭，防止匿名调用越权 |
| `StopHttp = true` | 内部接口（核心扣款、内部计算）防止外部直接 HTTP 调用 |
| `LockKey = ...` | 写操作类接口（对账、补单）防止并发执行 |
| `RateLimit = 60/m` | 公开接口（验证码、登录）防爬虫 |
| `LogParam = true` | 支付/审计类接口记录请求 |
| `ResponseType = HTTP` | 标准协议需要状态码、重定向、XML/纯文本或特定响应头 |

`ResponseType=HTTP` 不是任意响应头旁路。普通 V8 只能返回安全白名单头，`Location` 经过站内/HTTPS 校验；Host、Content-Length、逐跳头始终禁止。`Set-Cookie` 等高风险头只接受平台可信原子的进程内签名结果，V8 不能获得或伪造签名密钥。需要协议编解码、签名验签或密钥隔离时，公开地址仍由 Managed 接口引擎拥有，C# 只实现精确 Key 可调用的最小 `V8.Method` 原子，禁止恢复业务 Controller。

`ApiAddress` 使用 `{OsClient}`、`{ConnectionKey}` 路径模板时，模板值是权威值并覆盖同名 Query/Form/JSON 参数；多模板歧义必须失败关闭，防止跨租户或跨连接参数混淆。

### 表单上传的防篡改边界

- `ImgUpload/FileUpload/RichText` 的“禁止匿名访问”属于权威业务配置，不是用户等级策略。普通用户通过当前菜单、表和新增/编辑动作授权后，后端应重新读取 `diy_field.Config` 决定公有桶或私有桶；禁止把所有非超级管理员上传一刀切为私有桶。
- 恢复公有字段语义不能退回信任客户端 `Limit=false`。请求必须携带表、字段、菜单、记录及必要的 TableChild 上下文，后端校验字段归属、组件类型和操作权限，并固定控件对应的安全一级目录。
- 没有字段上下文的普通上传默认保持私有安全目录；管理员可在普通 `sys_config.HdfsUploadRules` 按可信角色授予业务目录（含有界 glob）与明确公有权限。必须先拒绝实际路径中的通配符、穿越和平台保留目录，再做模式匹配；通配符不是匿名/跨租户/发布授权。微信待审图片、头像、裁剪/压缩原图等更强规则继续优先。测试同时证明公有字段/目录可用、私有请求/伪造上下文/保留目录不可绕过。

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=v8-security-002 sha256=7229e5f8d200c0dfbab08752405f5e88d7edc80e9e92b6802880c022b09ca893 -->
## 1. 防 SQL 注入

### 必须：参数化查询

```javascript
// ✅ 使用 _Where（自动参数化）
V8.FormEngine.GetTableData('SysUser', {
  _Where: [['Account', '=', V8.Param.account]],
  _PageSize: 20
});

// ✅ 必须原生 SQL 时：FromSql 只传 SQL，参数用 AddInParameter
V8.Db.FromSql('SELECT * FROM SysUser WHERE Account = @p0')
  .AddInParameter("@p0", V8.Param.account)
  .ToArray();
```

### 禁止：字符串拼接

```javascript
// ❌ 绝对禁止
V8.Db.FromSql("SELECT * FROM SysUser WHERE Account = '" + V8.Param.account + "'").ToArray();

// ❌ 禁止动态拼接表名/字段名
V8.Db.FromSql("SELECT * FROM " + V8.Param.table).ToArray();
```

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=v8-security-003 sha256=8813dffa5c5c4c8816abddf3628579d474fe02137504d6c909f36303d4692560 -->
## 3. 输入验证

### 必填校验

```javascript
if (!V8.Param.name || !V8.Param.phone) {
  return { Code: 0, Msg: '姓名和手机号不能为空' };
}
```

### 格式校验

```javascript
// 手机号
if (V8.Param.phone && !/^1[3-9]\d{9}$/.test(V8.Param.phone)) {
  return { Code: 0, Msg: '手机号格式不正确' };
}

// 邮箱
if (V8.Param.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(V8.Param.email)) {
  return { Code: 0, Msg: '邮箱格式不正确' };
}

// ID 格式（GUID）
if (V8.Param.id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(V8.Param.id)) {
  return { Code: 0, Msg: 'ID 格式不正确' };
}
```

### 数值范围

```javascript
var pageSize = parseInt(V8.Param.pageSize) || 20;
pageSize = Math.max(1, Math.min(pageSize, 100));  // 限制 1~100

var amount = parseFloat(V8.Param.amount);
if (isNaN(amount) || amount <= 0 || amount > 999999.99) {
  return { Code: 0, Msg: '金额不合法' };
}
```

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=v8-security-004 sha256=1703ea824e4807b24d30225d41e16fb36b0fecc21c63a4ab0296106185460a27 -->
## 4. 防 XSS

四个字符替换不是通用 XSS 防护。必须按输出上下文处理：

- 纯文本保存原始业务值，渲染时使用 Vue 文本绑定/`textContent`，不要用 `v-html`；
- URL 只允许明确协议和域名，并通过 URL 解析器校验；
- 富文本使用平台统一 allowlist 清洗器，移除 `script`、事件属性、危险协议、`iframe/object` 等；
- V8 模板经 `v-safe-html` / DOMPurify 清洗，内联 `onclick` 等事件会被移除；交互使用平台按钮/V8 事件，不拼接可执行 HTML；
- CSV、Excel、邮件和日志还要分别处理公式注入、HTML 邮件和日志换行注入。

```javascript
var content = String(V8.Param.content || '');
if (content.length > 5000) {
  return { Code: 0, Msg: '内容过长' };
}
V8.FormEngine.AddFormData('Comment', {
  Content: content,
  UserId: V8.CurrentUser.Id
});
```

<!-- /microi-progressive:chunk -->
## 详细参考路由（渐进披露）

仅在当前任务涉及对应主题时读取；下列文件合计保留了原 SKILL.md 的全部详细知识。

- [references/progressive-01-2-权限校验.md](references/progressive-01-2-权限校验.md)：2. 权限校验；5. 防重复提交；6. 敏感数据
- [references/progressive-02-7-日志记录.md](references/progressive-02-7-日志记录.md)：7. 日志记录；8. 错误处理；9. Token、终端会话与租户隔离；10. Jint 运行时升级边界；安全检查清单；浏览器访问密钥
<!-- microi-progressive:end -->
