diff --git a/docs/security-findings.md b/docs/security-findings.md
new file mode 100644
index 0000000..4562dc1
--- /dev/null
+++ b/docs/security-findings.md
@@ -0,0 +1,42 @@
+# 现网安全发现报告(xlyAi / saas ERP)
+
+> 调查时间 2026-07-21。对象:saas-8s+ ERP 后端(`xlyEntry`)+ xlyAi 现有 NL2SQL。
+> 结论:**后端不是权限权威**,且现有 AI 取数路径存在越权与"读接口可写"风险。下述为供业务方决策的整改清单。
+
+## 摘要(按严重度)
+
+| # | 严重度 | 发现 | 影响 |
+|---|--------|------|------|
+| 1 | 高 | 后端逐用户**表单/菜单权限被故意关闭** | 任何登录用户经 API 可碰到 UI 里看不到的表单、可审核/删除全公司数据 |
+| 2 | 高 | 通用读接口**可被当写接口用** | `bUpdate=1` 等参数经参数绑定触发存储过程改库存 |
+| 3 | 高 | 现有 NL2SQL **无租户过滤、可跨品牌** | A 品牌用户"查所有客户"能拿到全部品牌数据 |
+| 4 | 中 | NL2SQL **执行期无表白名单**、SQL 安全仅部分 | 构造问题可 SELECT 任意视图;`INTO OUTFILE`/`LOAD_FILE` 未挡 |
+| 5 | 中 | NL2SQL **流式缓存命中跳过校验**、缓存无租户隔离 | 缓存投毒 / 越权复用 |
+
+## 明细
+
+### 1. 后端不是权限权威(表单级权限被关)
+- `AuthorizationInterceptor` 里逐用户表单权限校验 `checkByUser`(约 143–164 行)**被注释**,注释写着「朱总说不用放 20230626」。
+- `getBusinessDataByFormcustomId` 及**所有写/动作端点**(add/update/delete/审核 `doExamine`)只挂 `@Authorization`=**仅验登录**。
+- 后端**只强制**:公司级租户隔离(`sBrandsId/sSubsidiaryId`)+ 行级 `jurisdiction` 数据范围。
+- 逐用户表单/菜单权限只在**前端 UI** 按 `sAuthsId` 过滤菜单。**agent/脚本直接打 API 即绕过 UI** → 越权放大。
+- 说明:`sAuthsId` 的数据仍在(`sysjurisdiction`,由 `getsAuthsIdNew` 运行时算出,前端 `/getMenuList` 用),所以**可在 agent 侧补回表单级白名单**(xlyAi 已按此实现授权层,见 `docs/agent-architecture.md §7`)。
+
+### 2. 读接口可写
+- 通用表单接口把请求体任意 key 按名绑定到存储过程 IN 参、**无白名单**。
+- 例:`材料库存台账`(`SP_Inventory_InOutWarehouse`,AI 已暴露)在 `bUpdate=1` 时**真改** `MitMaterialsStore/EleMaterialsStock`(重算并持久化库存)。
+- 缓解:xlyAi 的 Read 工具只传分页/过滤参数,**绝不透传 `bUpdate/bUpdateAll`**(已实现)。根因需后端加**参数白名单**。
+
+### 3–5. 现有 NL2SQL(`XlyErpService.getDynamicTableSqlExec` 链路)
+- **无租户过滤**:prompt 从不提品牌,代码不追加 WHERE,视图把品牌当普通列但不过滤;规则默认「返回全部数据」→ 跨品牌泄漏。
+- **越权双轴**:跨品牌(同上);跨表——工具权限只筛「喂给模型的表」,**执行期无 allowlist**,构造问题可查任意视图;系统管理员拿到全部工具。
+- **SQL 安全仅部分**:jsqlparser 挡了 Insert/Update/Delete/Drop + 关键字黑名单,但**无正向 SELECT-only**、`INTO OUTFILE` 未挡、`LOAD_FILE` 未挡(`\bLOAD\b` ≠ `LOAD_FILE`)、无 LIMIT。本地账号 root 全权含 FILE。
+- **流式缓存路径命中即跳过校验**(`if isEmpty(cleanSql)` 兜底)+ 缓存无租户维度。
+
+## 建议整改(优先级)
+1. **后端**:重开逐用户表单权限校验(`checkByUser`),或至少对 AI 服务账号强制表单白名单。
+2. **后端**:通用读接口加**参数白名单**,禁止读路径接受写类参数(`bUpdate` 等)。
+3. **NL2SQL**(新 `queryData` 已按此实现):强制单条 SELECT、挡 OUTFILE/LOAD_FILE/系统库、强制 LIMIT、专用只读账号、AST 注入租户谓词、缓存过校验且按品牌隔离。
+4. 金融类数据:写操作/SQL **不可变审计**(xlyAi 已建 `ai_audit_log`)。
+
+> 注:xlyAi 侧已实现的缓解(授权层白名单、Read 参数白名单、`queryData` 安全栈、审计)是**纵深防御**,不替代后端整改(第 1、2 项根因在后端)。
diff --git a/src/main/java/com/xly/service/AuthzService.java b/src/main/java/com/xly/service/AuthzService.java
new file mode 100644
index 0000000..2086d9b
--- /dev/null
+++ b/src/main/java/com/xly/service/AuthzService.java
@@ -0,0 +1,137 @@
+package com.xly.service;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * xlyAi 侧授权层 —— 表单级白名单(架构 §7)。
+ *
+ *
后端逐用户表单权限被关(只验登录 + 租户 + 行级),所以在 agent 侧按用户实际授权把可碰的
+ * 表单/菜单限制住。数据源与后端一致(`sysjurisdiction`,即 `getsAuthsIdNew` 的逻辑)——不新造。
+ *
+ *
管理员(sysadmin/admin) = 全部权限(返回 null);否则 = 该用户有权的 id 集合(菜单/表单/按钮,
+ * 由 `sysjurisdiction.sKey` 按 '-' 拆出)。当前本地 dev-login 以 admin 身份运行,因此实际全通;
+ * 生产透传用户 token 时即按各用户真实授权收紧。Read / Query / Write 共用此边界。
+ */
+@Service
+public class AuthzService {
+
+ private final JdbcTemplate jdbc;
+
+ @Value("${erp.dev-login.username:admin}")
+ private String devUserNo;
+ @Value("${erp.dev-login.brand:1111111111}")
+ private String devBrand;
+ @Value("${erp.dev-login.subsidiary:1111111111}")
+ private String devSub;
+ @Value("${erp.dev-login.usertype:sysadmin}")
+ private String devUserType;
+
+ public AuthzService(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ /** 当前有效用户(本地=dev-login)能否访问该菜单/表单 id。 */
+ public boolean canAccessModule(String moduleId) {
+ Set granted = grantedIds(resolveDevUserId(), devUserType, devBrand, devSub);
+ return isAllowed(granted, moduleId);
+ }
+
+ /** null = 全部(管理员);否则 = 有权的 id 集合。 */
+ public Set grantedIds(String userId, String userType, String brandsId, String subsidiaryId) {
+ if (isAdmin(userType)) {
+ return null; // 超管全部权限
+ }
+ Set ids = new HashSet<>();
+ if (userId == null) {
+ return ids;
+ }
+ List