Преглед изворни кода

Merge branch 'bjcz_his_scrm' of http://1.14.104.71:10880/root/ylrz_his_scrm_java into bjcz_his_scrm

xw пре 2 дана
родитељ
комит
ac7b163533
74 измењених фајлова са 979 додато и 187 уклоњено
  1. 105 0
      docs/高危漏洞修复报告-补充-无感加固.md
  2. 273 0
      docs/高危漏洞修复报告.md
  3. 1 1
      fs-ad-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  4. 1 1
      fs-ad-new-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  5. 13 3
      fs-admin/src/main/java/com/fs/chat/controller/ChatDatasetFileController.java
  6. 3 4
      fs-admin/src/main/java/com/fs/web/controller/common/CommonController.java
  7. 2 0
      fs-admin/src/main/java/com/fs/web/controller/system/SysUserController.java
  8. 6 0
      fs-admin/src/main/resources/application.yml
  9. 1 1
      fs-common-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  10. 5 3
      fs-common/src/main/java/com/fs/common/utils/SortUtils.java
  11. 2 2
      fs-common/src/main/java/com/fs/common/utils/file/MimeTypeUtils.java
  12. 58 0
      fs-common/src/main/java/com/fs/common/utils/security/OssUploadSecurityUtils.java
  13. 67 0
      fs-common/src/main/java/com/fs/common/utils/security/SqlSafeUtils.java
  14. 58 0
      fs-common/src/main/java/com/fs/common/utils/security/UrlSecurityUtils.java
  15. 29 0
      fs-common/src/main/java/com/fs/common/utils/security/XmlSecurityUtils.java
  16. 4 4
      fs-company-app/src/main/java/com/fs/app/controller/CommonController.java
  17. 3 0
      fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  18. 32 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java
  19. 17 0
      fs-company-app/src/main/java/com/fs/app/controller/UserController.java
  20. 1 1
      fs-company-app/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java
  21. 13 3
      fs-company/src/main/java/com/fs/chat/controller/ChatDatasetFileController.java
  22. 3 3
      fs-company/src/main/java/com/fs/chat/controller/ChatUploadController.java
  23. 9 10
      fs-company/src/main/java/com/fs/company/controller/common/CommonController.java
  24. 11 3
      fs-company/src/main/java/com/fs/company/utils/AudioUtils.java
  25. 1 1
      fs-company/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  26. 4 4
      fs-doctor-app/src/main/java/com/fs/app/controller/CommonController.java
  27. 1 1
      fs-doctor-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  28. 1 1
      fs-framework/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  29. 7 20
      fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java
  30. 1 1
      fs-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  31. 1 1
      fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  32. 1 1
      fs-live-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  33. 1 1
      fs-mp-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  34. 1 1
      fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  35. 3 3
      fs-qw-api/src/main/java/com/fs/app/controller/CommonController.java
  36. 3 3
      fs-qw-api/src/main/java/com/fs/app/controller/QwController.java
  37. 1 1
      fs-qw-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  38. 1 1
      fs-qw-company-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  39. 1 1
      fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  40. 1 1
      fs-qw-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  41. 1 1
      fs-qw-voice/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  42. 3 2
      fs-qwhook-msg/src/main/java/com/fs/app/utils/AudioUtils.java
  43. 1 1
      fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  44. 3 2
      fs-qwhook-sop/src/main/java/com/fs/app/utils/AudioUtils.java
  45. 1 1
      fs-qwhook-sop/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  46. 3 2
      fs-qwhook/src/main/java/com/fs/app/utils/AudioUtils.java
  47. 1 1
      fs-qwhook/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  48. 1 1
      fs-redis/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  49. 1 1
      fs-repeat-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  50. 6 3
      fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoServiceImpl.java
  51. 12 7
      fs-service/src/main/java/com/fs/course/service/impl/HuaweiObsServiceImpl.java
  52. 9 7
      fs-service/src/main/java/com/fs/course/service/impl/HuaweiVodServiceImpl.java
  53. 5 11
      fs-service/src/main/java/com/fs/fastGpt/service/impl/FastGptCollectionServiceImpl.java
  54. 5 2
      fs-service/src/main/java/com/fs/fastgptApi/util/AudioUtils.java
  55. 8 0
      fs-service/src/main/java/com/fs/his/param/FsStoreOrderParam.java
  56. 8 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderParam.java
  57. 8 0
      fs-service/src/main/java/com/fs/live/param/LiveOrderParam.java
  58. 4 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  59. 25 9
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  60. 4 3
      fs-service/src/main/java/com/fs/live/utils/ProcessManager.java
  61. 7 0
      fs-service/src/main/java/com/fs/qwApi/util/XMLParse.java
  62. 14 1
      fs-service/src/main/java/com/fs/sms/service/impl/SmsTServiceImpl.java
  63. 1 1
      fs-store/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  64. 5 6
      fs-store/src/main/java/com/fs/store/controller/common/CommonController.java
  65. 1 1
      fs-user-app-ai-chat/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  66. 38 24
      fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java
  67. 4 4
      fs-user-app/src/main/java/com/fs/app/controller/CommonController.java
  68. 15 7
      fs-user-app/src/main/java/com/fs/app/controller/TalentController.java
  69. 11 1
      fs-user-app/src/main/java/com/fs/app/controller/WxPayController.java
  70. 3 3
      fs-user-app/src/main/java/com/fs/app/controller/store/CommonScrmController.java
  71. 22 1
      fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java
  72. 1 1
      fs-user-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  73. 1 1
      fs-watch/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  74. 1 1
      fs-wx-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

+ 105 - 0
docs/高危漏洞修复报告-补充-无感加固.md

@@ -0,0 +1,105 @@
+# 补充修复报告:不影响业务逻辑的安全加固
+
+> **筛选原则**:只修复「正常请求路径不变、仅加固防护」的漏洞;不改权限体系、不改鉴权模型、不强制变更业务流程。  
+> **完成时间**:2026-08-06  
+> **编译验证**:`mvn -pl fs-common,fs-framework,fs-service,fs-user-app,fs-company,fs-qw-api,fs-admin,fs-qwhook -am compile -DskipTests` 通过  
+
+---
+
+## 一、本轮筛选结论
+
+| 类型 | 是否本轮修复 | 原因 |
+|------|--------------|------|
+| XXE 禁用 DTD | **已修** | 仅加强 XML 解析器特性,正常企微明文 XML 不受影响 |
+| Runtime.exec → ProcessBuilder | **已修** | 命令参数相同,仅调用方式更安全 |
+| 禁止 html/htm 上传/下载白名单 | **已修** | HIS 业务一般不需要上传可渲染 HTML;可阻断存储型 XSS |
+| 剩余裸 uploadOSS | **已修** | 复用已有白名单工具,合法类型仍可用 |
+| 安全响应头 nosniff | **已修** | 仅增加响应头,不影响业务数据 |
+| 知识库上传文件名清洗 | **已修** | 仅过滤危险字符,中英文名仍可用 |
+| Jackson DefaultTyping 收紧 | **未改** | 可能影响 Redis 历史缓存反序列化,风险大于收益,需单独评估 |
+| 后台补 `@PreAuthorize` | **未改** | 依赖权限点配置,可能造成后台 403 |
+| fs-company-app `/**` permitAll | **未改** | 改动会冲击现有 `@Login` 拦截体系 |
+| 跨租户 companyId 强制覆盖 | **未改** | 可能影响运营超管跨公司操作 |
+
+---
+
+## 二、已修复明细
+
+### 1. XXE(中危-01 等)
+
+| 文件 | 改动 |
+|------|------|
+| 新增 `fs-common/.../XmlSecurityUtils.java` | 统一禁用 DTD/外部实体 |
+| `fs-qw-api/.../QwController.java` | 使用 `XmlSecurityUtils.newSafeDocumentBuilder()` |
+| `fs-service/.../qwApi/util/XMLParse.java` | 补齐与官方安全示例一致的 feature |
+
+**影响**:正常企微回调 XML 无 DOCTYPE,解析结果不变。
+
+### 2. 命令注入(中危-03/04/05)
+
+| 文件 | 改动 |
+|------|------|
+| `HuaweiVodServiceImpl` / `HuaweiObsServiceImpl` | `Runtime.exec(拼接串)` → `ProcessBuilder` 参数数组 |
+| `AudioUtils`(company / qwhook / qwhook-msg / qwhook-sop / fastgptApi) | silk 编码改为 `ProcessBuilder` |
+| `ProcessManager` + `LiveServiceImpl#buildFFmpegCommand` | 改为 `List<String>` 传参,路径不再被空格拆开 |
+
+**影响**:ffmpeg / silk 参数语义不变,路径含空格时反而更稳。
+
+### 3. 存储型 XSS / 上传(中危-07、剩余高危-08)
+
+| 文件 | 改动 |
+|------|------|
+| `MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION` | **移除 `html`/`htm`**(保留 txt/文档/图片/视频/pdf) |
+| `fs-qw-api/.../CommonController#uploadOSS` | 接入 `OssUploadSecurityUtils` |
+| `fs-user-app/.../CommonScrmController#uploadOSS` | 接入 `OssUploadSecurityUtils` |
+
+**影响**:若业务确有上传 `.html` 页面需求会被拒绝;常规图片/文档/音视频不受影响。
+
+### 4. 响应头(低危 nosniff)
+
+| 文件 | 改动 |
+|------|------|
+| `fs-framework/.../SecurityConfig.java` | 增加 `.headers().contentTypeOptions()` |
+
+**影响**:仅增加 `X-Content-Type-Options: nosniff`,不影响接口数据。
+
+### 5. 知识库文件名(中危-10)
+
+| 文件 | 改动 |
+|------|------|
+| `fs-admin` / `fs-company` `ChatDatasetFileController#getFileNameWithoutExtension` | 白名单过滤危险字符,长度限制 50 |
+
+**影响**:含特殊符号的文件名会被清洗,中英文与数字下划线不受影响。
+
+---
+
+## 三、明确跳过(避免影响使用)
+
+1. **Jackson `activateDefaultTyping(LaissezFaire...)`**  
+   全仓库大量 RedisConfig 使用,贸然改为白名单可能导致登录态/缓存读失败。建议单独做兼容验证后再改。
+
+2. **垂直越权补权限注解 / 强制 companyId**  
+   会改变后台可见数据范围或触发 403,需产品与权限表配合。
+
+3. **优惠券竞态加分布式锁**  
+   属逻辑增强,可能改变高并发领取时序,需业务确认。
+
+4. **下载接口去掉客户端 `delete` 参数**  
+   若前端依赖该参数清理临时文件,直接删除会改变行为;建议业务确认后改。
+
+---
+
+## 四、建议回归点(轻量)
+
+1. 企微回调是否正常(XXE 加固后)  
+2. 上传图片/PDF/xlsx/mp4 是否正常;确认 `.html` 被拒绝符合预期  
+3. 语音转 silk / 视频 ffmpeg 合并是否正常  
+4. 直播录播推流(FFmpeg List 传参)是否正常  
+5. 知识库 Excel 导入文件名是否正常显示  
+
+---
+
+## 五、总结
+
+本轮只做了**无感安全加固**:XXE、命令执行方式、html 上传封禁、剩余 OSS 白名单、nosniff、文件名清洗。  
+不涉及鉴权模型、权限点、支付主流程、租户隔离策略,正常业务使用面基本不受影响。

+ 273 - 0
docs/高危漏洞修复报告.md

@@ -0,0 +1,273 @@
+# ylrz_his_scrm_java 高危漏洞核实与修复报告
+
+> **依据文档**:`安全漏洞审计报告.md`  
+> **核实范围**:本地仓库 `ylrz_his_scrm_java`  
+> **修复原则**:仅对已核实存在的高危项做最小安全加固,不改动正常业务主流程;支付/登录/上传等接口在原有逻辑上增加校验或关闭后门  
+> **修复完成时间**:2026-08-06  
+> **编译验证**:`mvn -pl fs-common,fs-framework,fs-service,fs-user-app,fs-company-app,fs-admin -am compile -DskipTests` 通过  
+
+---
+
+## 一、核实结论总览
+
+审计报告列出高危约 **36** 项(含编号重叠)。本地核实结果如下:
+
+| 结论 | 数量 | 说明 |
+|------|------|------|
+| **已确认存在并已修复** | 28 | 见下文「已修复清单」 |
+| **报告描述存在,当前仓库不存在** | 4 | 高危-02/03/07/16 对应文件或接口不在本仓库 |
+| **存在但未做破坏性大改(有说明)** | 4 | 如 fs-company-app 全量 `permitAll`、部分后台垂直越权需配权限位 |
+| **额外发现并已处理** | 1 | 硬编码万能短信验证码后门 |
+
+---
+
+## 二、不存在 / 与报告不一致的项
+
+| 编号 | 报告描述 | 核实结果 |
+|------|----------|----------|
+| 高危-02 | `CrmBusinessMapper.xml` orderBy 注入 | **不存在**:`mapper/crm` 下无 `CrmBusinessMapper.xml` |
+| 高危-03 | `CrmCustomerMapper.xml` `${orderBy}` | **不存在**:该 XML 无 `${orderBy}` / `${maps.orderBy}` 拼接点 |
+| 高危-07 | `CrmExtDetailMapper.xml` 动态 DDL | **不存在**:无该 Mapper 文件 |
+| 高危-16 | `loginByUnionId` 后门 | **不存在**:`AppLoginController` 无此接口(仅有合法微信 code 换取 unionId 流程) |
+| 高危-24 | `getUserInfoByUserId` 无 `@Login` | **当前未发现该接口**(或已移除),不纳入本次改动 |
+| 高危-25/26 | admin 跨公司员工列表 | 与高危-21 同类;本次未做大规模权限重构,避免影响运营后台菜单权限体系,列入后续项 |
+
+---
+
+## 三、已修复清单(按报告编号)
+
+### A. SQL 注入
+
+#### [高危-01] LiveDataMapper `${category}` — **已修复**
+- **核实**:`LiveDataMapper.xml` L142/L192 存在 `SUM(ld.${category})`
+- **修复位置**:
+  - 新增 `fs-common/.../security/SqlSafeUtils.java`(`validateLiveCategory`)
+  - `LiveDataServiceImpl#getTrendData` / `getCompanyTrendData` 入口白名单校验  
+    仅允许:`page_views`、`unique_visitors`、`total_views`、`unique_viewers`、`streams`
+- **业务影响**:非法 category 直接拒绝;合法统计维度不变
+
+#### [高危-04][高危-05] sortOrder 排序注入 — **已修复**
+- **核实**:`FsStoreOrderScrmMapper.xml` / `LiveOrderMapper.xml` 等使用 `${maps.sortOrder}`,且位于 `ORDER BY <字段>` 之后,语义为 asc/desc
+- **修复位置**:
+  - `FsStoreOrderParam`(his / hisStore)自定义 `setSortOrder`
+  - `LiveOrderParam` 自定义 `setSortOrder`
+  - 统一走 `SqlSafeUtils.validateOrderDirection`
+
+#### [高危-06] SortUtils 排序字段名注入 — **已修复**
+- **核实**:`SortUtils.parseSort` 仅校验方向,未校验字段名
+- **修复位置**:`fs-common/.../SortUtils.java`  
+  字段名仅允许 `[a-zA-Z0-9_]+`,方向仅允许 asc/desc
+
+---
+
+### B. 上传 / 路径穿越 / SSRF
+
+#### [高危-08] uploadOSS 无后缀白名单 — **已修复(核心模块)**
+- **新增**:`OssUploadSecurityUtils`(安全后缀白名单,排除 html/jsp 等)
+- **修复文件**:
+  - `fs-company/.../CommonController.java`
+  - `fs-admin/.../CommonController.java`(含 `uploadWang`)
+  - `fs-store/.../CommonController.java`
+  - `fs-company-app/.../CommonController.java`
+  - `fs-user-app/.../CommonController.java`
+  - `fs-doctor-app/.../CommonController.java`
+  - `fs-company/.../ChatUploadController.java`
+- **说明**:其余模块若仍有同类 `uploadOSS` 副本,建议继续复用 `OssUploadSecurityUtils`
+
+#### [高危-09] uploadOSS2 原始文件名路径注入 — **已修复**
+- **修复**:改为 `uploadSuffix` + 白名单,不再用原始文件名构造 OSS key  
+  (`fs-company` CommonController 等)
+
+#### [高危-10] TalentController 路径穿越 + 未鉴权 — **已修复**
+- **修复位置**:`fs-user-app/.../TalentController.java`
+  - 恢复 `@Login`
+  - 后缀白名单
+  - 本地落盘改用 UUID 文件名,并做 canonical path 校验
+
+#### [高危-11] 声纹 URL SSRF — **已修复**
+- **新增**:`UrlSecurityUtils.validatePublicHttpUrl`
+- **修复位置**:
+  - `CompanyUserController#addVoicePrintUrl`
+  - `AudioUtils.audioWAVFromUrl`(`fs-service/.../fastgptApi/util/AudioUtils.java`)
+
+#### [高危-12] FastGpt `urlToFile` SSRF — **已修复**
+- **修复位置**:`FastGptCollectionServiceImpl#urlToFile`  
+  URL 校验 + 临时文件固定 `.tmp` 后缀
+
+#### [高危-13] 视频临时文件后缀可控 — **已修复**
+- **修复位置**:`FsUserVideoServiceImpl#uploadVideo`  
+  仅允许 `.mp4`,临时文件固定 `.tmp`
+
+---
+
+### C. 鉴权与权限
+
+#### [高危-14][高危-15] `loginByUserId` / `loginTest` — **已修复(禁用)**
+- **修复位置**:`AppLoginController`
+- **方式**:保留路径,统一返回 `403 该接口已禁用`,并打 warn 日志  
+  (避免直接删方法导致部分客户端 404 误判;功能上已不可登录)
+
+#### [高危-17] fs-company-app `/**` permitAll — **未改为 Spring Security 强制认证**
+- **原因**:该模块依赖自定义 `@Login` + 拦截器;直接改为 `anyRequest().authenticated()` 会大面积打断现有 JWT 体系,属于逻辑性破坏
+- **本次替代措施**:补齐关键接口 `@Login` / 归属校验(见高危-28/29)
+- **建议**:单独立项做 Spring Security 与拦截器统一
+
+#### [高危-18][高危-19][高危-23] 匿名放行过宽 — **已修复(fs-framework SecurityConfig)**
+- **修复位置**:`fs-framework/.../SecurityConfig.java`
+- **已移除匿名**:
+  - `/common/uploadOSS**`、`/common/uploadWang**`、下载类、`unbindQwUserByServerIds`
+  - `/his/data/*` 系列危险运维接口
+  - Swagger / Druid 匿名
+- **仍保留匿名**:支付回调、短信回调等第三方回调必要路径
+
+#### [高危-20][高危-21][高危-22] 后台大量缺 `@PreAuthorize` — **部分未改**
+- **原因**:补权限注解依赖菜单权限点是否已配置,盲目添加会导致正常后台功能 403
+- **建议**:按菜单权限表分批补齐(P1)
+
+---
+
+### D. 数据越权
+
+#### [高危-28] changeUserInfo / disabled IDOR — **已修复**
+- **修复位置**:`fs-company-app/.../FsUserController.java`
+  - `changeUserInfo`:校验 `fs_user_company_user` 归属当前销售
+  - `changeUserRemark`:校验归属
+  - `disabled` / `enabled`:批量校验归属
+
+#### [高危-29] delMyCard / changeUserState / getUserList — **已修复**
+- **修复位置**:`fs-company-app/.../UserController.java`
+  - `delMyCard`:校验 `card.userId == 当前用户`
+  - `changeUserState`:管理员 + 同公司校验
+  - `getUserList`:补 `@Login`
+
+---
+
+### E. 逻辑漏洞
+
+#### [高危-30] 微信支付 switch fall-through — **已修复**
+- **修复位置**:`WxPayController#wxPayNotify`  
+  每个 case 增加 `break`,并增加 `default` 日志  
+- **说明**:注释已标明该接口不可用、应走 `/store/app/wxpay/wxPayNotify`;本次仍修复,防止误调用造成重复入账
+
+#### [高危-31] 汇付回调未验签 — **已修复**
+- **修复位置**:`PayScrmController#hfPayNotify`  
+  使用 `JsonUtils.sort4JsonString` + `RsaUtils.verify` + `BasePay.HUIFU_DEFAULT_PUBLIC_KEY` 先验签,失败返回 `fail`
+
+#### [高危-32] 短信验证码可复用 / 可爆破 — **已修复**
+- **修复位置**:`AppLoginController`
+  - `resetPassword`:失败计数 ≥5 锁定 30 分钟;成功后删除验证码
+  - `loginByPhone` / `registerByPhone`:成功后删除验证码
+
+#### [高危-33] 默认密码 zm12345678 — **当前仓库未命中**
+- 未检索到 `zm12345678` / `getUserDefaultPassword` 自动设密逻辑;无需改动
+
+#### [高危-34] 跨租户重置密码 — **已修复**
+- **修复位置**:`SysUserController#resetPwd`  
+  增加 `userService.checkUserDataScope(user.getUserId())`
+
+---
+
+### F. 反序列化与凭据
+
+#### [高危-35] Fastjson AutoType — **已修复**
+- **修复**:全仓库 **27** 个 `FastJson2JsonRedisSerializer.java`  
+  `setAutoTypeSupport(true)` → `false`
+
+#### [高危-36] 硬编码腾讯云 AK/SK — **已修复**
+- **修复位置**:`SmsTServiceImpl`
+  - 改为 `@Value("${tencent.cloud.secret-id/secret-key}")`
+  - `fs-admin/.../application.yml` 增加环境变量注入占位
+- **运维必做**:
+  1. **立即在腾讯云控制台禁用并轮换**旧密钥 `AKIDdAdvzoQpzuOo...`
+  2. 各环境设置:
+     - `TENCENT_CLOUD_SECRET_ID`
+     - `TENCENT_CLOUD_SECRET_KEY`
+  3. 未配置时短信发送会返回明确错误,避免静默使用硬编码密钥
+
+---
+
+## 四、额外发现并处理
+
+| 项 | 说明 | 处理 |
+|----|------|------|
+| 硬编码万能短信验证码 | `AppLoginController` 原 `INTERNAL_LOGIN_CODE = "EdisonX!@#.+-123"`,可绕过短信登录任意用户 | 已清空禁用;仅当配置非空内部码时才可能生效(当前为空) |
+
+---
+
+## 五、新增公共安全组件
+
+| 类 | 路径 | 作用 |
+|----|------|------|
+| `SqlSafeUtils` | `fs-common/.../security/SqlSafeUtils.java` | 排序方向 / 列名 / live category 白名单 |
+| `UrlSecurityUtils` | `fs-common/.../security/UrlSecurityUtils.java` | SSRF:协议白名单 + 拒绝内网地址 |
+| `OssUploadSecurityUtils` | `fs-common/.../security/OssUploadSecurityUtils.java` | OSS 上传后缀白名单 + 文件名穿越过滤 |
+
+---
+
+## 六、隐患与后续建议(请确认)
+
+1. **腾讯云旧密钥必须轮换**:源码历史中已暴露,仅改代码不够。  
+2. **汇付验签公钥**:当前使用 `BasePay.HUIFU_DEFAULT_PUBLIC_KEY`;若商户使用自定义公钥,需改为按商户配置取公钥,否则合法回调可能验签失败。  
+3. **fs-company-app `/**` permitAll**:仍依赖方法级 `@Login`,遗漏注解即裸奔;建议后续统一鉴权。  
+4. **后台垂直越权(QwUser / CompanyUser / IpadAllocation)**:未批量加 `@PreAuthorize`,避免权限点未配置导致生产不可用;建议按权限表分批治理。  
+5. **其他模块 uploadOSS 副本**:已修核心 6 端 + Chat 上传;ad/qw 等模块若仍有裸上传,可继续复用 `OssUploadSecurityUtils`。  
+6. **关闭 AutoType**:若历史 Redis 缓存依赖 `@type` 多态反序列化,升级后偶发反序列化失败,需观察并清理旧缓存。  
+7. **SecurityConfig 取消 Swagger/Druid 匿名**:开发环境如需调试,可用 profile 单独放开,勿在生产匿名。
+
+---
+
+## 七、修复文件索引(主要)
+
+```
+fs-common/src/main/java/com/fs/common/utils/security/SqlSafeUtils.java          (新增)
+fs-common/src/main/java/com/fs/common/utils/security/UrlSecurityUtils.java       (新增)
+fs-common/src/main/java/com/fs/common/utils/security/OssUploadSecurityUtils.java (新增)
+fs-common/src/main/java/com/fs/common/utils/SortUtils.java
+fs-service/.../live/service/impl/LiveDataServiceImpl.java
+fs-service/.../hisStore/param/FsStoreOrderParam.java
+fs-service/.../his/param/FsStoreOrderParam.java
+fs-service/.../live/param/LiveOrderParam.java
+fs-service/.../course/service/impl/FsUserVideoServiceImpl.java
+fs-service/.../fastGpt/service/impl/FastGptCollectionServiceImpl.java
+fs-service/.../fastgptApi/util/AudioUtils.java
+fs-service/.../sms/service/impl/SmsTServiceImpl.java
+fs-user-app/.../AppLoginController.java
+fs-user-app/.../WxPayController.java
+fs-user-app/.../store/PayScrmController.java
+fs-user-app/.../TalentController.java
+fs-user-app/.../CommonController.java
+fs-company-app/.../CommonController.java
+fs-company-app/.../CompanyUserController.java
+fs-company-app/.../FsUserController.java
+fs-company-app/.../UserController.java
+fs-company/.../common/CommonController.java
+fs-company/.../chat/ChatUploadController.java
+fs-admin/.../common/CommonController.java
+fs-admin/.../system/SysUserController.java
+fs-admin/src/main/resources/application.yml
+fs-store/.../common/CommonController.java
+fs-doctor-app/.../CommonController.java
+fs-framework/.../SecurityConfig.java
+fs-*/.../FastJson2JsonRedisSerializer.java  (27 处)
+```
+
+---
+
+## 八、总结
+
+本次已对审计报告中**本地可核实的高危漏洞**完成加固,覆盖:
+
+- SQL 注入(category / sortOrder / 排序字段)
+- 上传白名单与路径穿越
+- SSRF
+- 登录后门禁用 + 万能验证码禁用
+- 支付 fall-through / 汇付验签
+- 短信验证码一次性与爆破限制
+- Fastjson AutoType
+- 云密钥去硬编码
+- 关键 IDOR 归属校验
+- 管理端危险匿名接口收敛
+
+**未做或需后续立项**的项主要是:CRM 报告中不存在的 Mapper、后台大面积 `@PreAuthorize` 补齐、fs-company-app Spring Security 体系重构。  
+
+部署前请务必完成:**腾讯云密钥轮换**、**各环境注入新密钥**、**汇付回调验签联调**、**上传白名单业务抽样回归**。

+ 1 - 1
fs-ad-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-ad-new-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 13 - 3
fs-admin/src/main/java/com/fs/chat/controller/ChatDatasetFileController.java

@@ -157,10 +157,20 @@ public class ChatDatasetFileController extends BaseController
         int dotIndex = nameWithExtension.lastIndexOf(".");
 
         // 检查文件名是否有扩展名
+        String name;
         if (dotIndex > 0 && dotIndex < nameWithExtension.length() - 1) {
-            return nameWithExtension.substring(0, dotIndex);
+            name = nameWithExtension.substring(0, dotIndex);
+        } else {
+            name = nameWithExtension;
         }
-
-        return nameWithExtension;
+        // 文件名字符白名单,防止路径穿越/特殊字符注入(不影响正常中英文名)
+        name = name.replaceAll("[^a-zA-Z0-9_\\-\\u4e00-\\u9fa5]", "");
+        if (name.length() > 50) {
+            name = name.substring(0, 50);
+        }
+        if (name.isEmpty()) {
+            name = "file";
+        }
+        return name;
     }
 }

+ 3 - 4
fs-admin/src/main/java/com/fs/web/controller/common/CommonController.java

@@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletResponse;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.course.dto.BatchSendCourseAllDTO;;
 import com.fs.course.service.ITencentCloudCosService;
 import com.fs.framework.config.ServerConfig;
@@ -174,8 +175,7 @@ public class CommonController
             throw new OssException("上传文件不能为空");
         }
         // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         CloudStorageService storage = OSSFactory.build();
         String url = storage.uploadSuffix(file.getBytes(), suffix);
         return R.ok().put("url",url);
@@ -191,8 +191,7 @@ public class CommonController
                 throw new OssException("上传文件不能为空");
             }
             // 上传文件
-            String fileName = file.getOriginalFilename();
-            String suffix = fileName.substring(fileName.lastIndexOf("."));
+            String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
             CloudStorageService storage = OSSFactory.build();
             String url = storage.uploadSuffix(file.getBytes(), suffix);
             vo.setErrno(0);

+ 2 - 0
fs-admin/src/main/java/com/fs/web/controller/system/SysUserController.java

@@ -211,6 +211,8 @@ public class SysUserController extends BaseController
     public AjaxResult resetPwd(@RequestBody SysUser user)
     {
         userService.checkUserAllowed(user);
+        // 增加数据权限校验,防止跨租户/跨部门重置密码
+        userService.checkUserDataScope(user.getUserId());
         user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
         user.setUpdateBy(getUsername());
         return toAjax(userService.resetPwd(user));

+ 6 - 0
fs-admin/src/main/resources/application.yml

@@ -12,3 +12,9 @@ spring:
 #    active: druid-fby
 #    active: dev
 
+# 腾讯云短信密钥(安全修复:禁止硬编码,请通过环境变量注入,并立即轮换旧密钥)
+tencent:
+  cloud:
+    secret-id: ${TENCENT_CLOUD_SECRET_ID:}
+    secret-key: ${TENCENT_CLOUD_SECRET_KEY:}
+

+ 1 - 1
fs-common-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 5 - 3
fs-common/src/main/java/com/fs/common/utils/SortUtils.java

@@ -1,6 +1,7 @@
 package com.fs.common.utils;
 
 import com.fs.common.core.domain.SortRule;
+import com.fs.common.utils.security.SqlSafeUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -26,11 +27,12 @@ public class SortUtils {
             }
 
             String field = split[0].trim().replace(" ", ""); // 字段名:a, b, c
+            // 防止排序字段名 SQL 注入:仅允许字母数字下划线
+            field = SqlSafeUtils.validateColumnName(field);
 
             String direction = split[1].replace(")", "").trim(); // 方向:desc
-            if (!"asc".equalsIgnoreCase(direction) && !"desc".equalsIgnoreCase(direction)) {
-                continue;
-            }
+            // 排序方向白名单
+            direction = SqlSafeUtils.validateOrderDirection(direction);
 
             rules.add(new SortRule(field, direction));
         }

+ 2 - 2
fs-common/src/main/java/com/fs/common/utils/file/MimeTypeUtils.java

@@ -29,8 +29,8 @@ public class MimeTypeUtils
     public static final String[] DEFAULT_ALLOWED_EXTENSION = {
             // 图片
             "bmp", "gif", "jpg", "jpeg", "png",
-            // word excel powerpoint
-            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+            // word excel powerpoint(已移除 html/htm,避免存储型 XSS)
+            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt",
             // 压缩文件
             "rar", "zip", "gz", "bz2",
             // 视频格式

+ 58 - 0
fs-common/src/main/java/com/fs/common/utils/security/OssUploadSecurityUtils.java

@@ -0,0 +1,58 @@
+package com.fs.common.utils.security;
+
+import com.fs.common.exception.file.InvalidExtensionException;
+import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.file.FileUploadUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * OSS 上传安全校验:后缀白名单,禁止可执行/脚本类型
+ */
+public final class OssUploadSecurityUtils {
+
+    /**
+     * 业务允许的安全后缀(不含 html/htm/jsp 等危险类型)
+     */
+    public static final String[] SAFE_ALLOWED_EXTENSION = {
+            // 图片
+            "bmp", "gif", "jpg", "jpeg", "png", "webp",
+            // 文档
+            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf",
+            // 压缩
+            "rar", "zip", "gz", "bz2",
+            // 音视频
+            "mp3", "wav", "mp4", "avi", "rmvb"
+    };
+
+    private OssUploadSecurityUtils() {
+    }
+
+    /**
+     * 校验文件并返回带点后缀,如 .jpg
+     */
+    public static String validateAndGetDotSuffix(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new OssException("上传文件不能为空");
+        }
+        String originalFilename = file.getOriginalFilename();
+        if (StringUtils.isEmpty(originalFilename)) {
+            throw new OssException("文件名不能为空");
+        }
+        if (originalFilename.contains("..") || originalFilename.contains("/") || originalFilename.contains("\\")) {
+            throw new OssException("文件名非法");
+        }
+        try {
+            FileUploadUtils.assertAllowed(file, SAFE_ALLOWED_EXTENSION);
+        } catch (InvalidExtensionException e) {
+            throw new OssException("不支持的文件类型: " + FileUploadUtils.getExtension(file));
+        } catch (Exception e) {
+            throw new OssException("文件校验失败: " + e.getMessage());
+        }
+        String extension = FileUploadUtils.getExtension(file);
+        if (StringUtils.isEmpty(extension)) {
+            throw new OssException("无法识别文件后缀");
+        }
+        return "." + extension.toLowerCase();
+    }
+}

+ 67 - 0
fs-common/src/main/java/com/fs/common/utils/security/SqlSafeUtils.java

@@ -0,0 +1,67 @@
+package com.fs.common.utils.security;
+
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.StringUtils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * SQL 动态片段安全校验工具(排序方向、列名白名单)
+ */
+public final class SqlSafeUtils {
+
+    private static final Set<String> ORDER_DIRECTIONS = new HashSet<String>(Arrays.asList("asc", "desc"));
+
+    /** 直播趋势统计允许的 category 列名 */
+    private static final Set<String> LIVE_CATEGORY_COLUMNS = new HashSet<String>(Arrays.asList(
+            "page_views", "unique_visitors", "total_views", "unique_viewers", "streams"
+    ));
+
+    private SqlSafeUtils() {
+    }
+
+    /**
+     * 校验排序方向,仅允许 asc/desc;空值直接返回
+     */
+    public static String validateOrderDirection(String order) {
+        if (StringUtils.isEmpty(order)) {
+            return order;
+        }
+        String lower = order.trim().toLowerCase(Locale.ROOT);
+        if (!ORDER_DIRECTIONS.contains(lower)) {
+            throw new ServiceException("排序方向非法,仅支持 asc/desc");
+        }
+        return lower;
+    }
+
+    /**
+     * 校验直播趋势 category,防止 ${category} SQL 注入
+     */
+    public static String validateLiveCategory(String category) {
+        if (StringUtils.isEmpty(category)) {
+            throw new ServiceException("category 不能为空");
+        }
+        String value = category.trim();
+        if (!LIVE_CATEGORY_COLUMNS.contains(value)) {
+            throw new ServiceException("非法的 category 参数");
+        }
+        return value;
+    }
+
+    /**
+     * 校验排序字段名:仅允许字母数字下划线,且长度受限
+     */
+    public static String validateColumnName(String column) {
+        if (StringUtils.isEmpty(column)) {
+            throw new ServiceException("排序字段不能为空");
+        }
+        String value = column.trim();
+        if (value.length() > 64 || !value.matches("^[a-zA-Z0-9_]+$")) {
+            throw new ServiceException("排序字段名非法: " + column);
+        }
+        return value;
+    }
+}

+ 58 - 0
fs-common/src/main/java/com/fs/common/utils/security/UrlSecurityUtils.java

@@ -0,0 +1,58 @@
+package com.fs.common.utils.security;
+
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.StringUtils;
+
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * 对外 URL 访问安全校验,防止 SSRF
+ */
+public final class UrlSecurityUtils {
+
+    private static final Set<String> ALLOWED_PROTOCOLS = new HashSet<String>(Arrays.asList("http", "https"));
+
+    private UrlSecurityUtils() {
+    }
+
+    /**
+     * 校验 URL:协议白名单 + 拒绝内网/回环/链路本地地址
+     */
+    public static void validatePublicHttpUrl(String urlStr) {
+        if (StringUtils.isEmpty(urlStr)) {
+            throw new ServiceException("URL 不能为空");
+        }
+        try {
+            URL url = new URL(urlStr);
+            String protocol = url.getProtocol() == null ? "" : url.getProtocol().toLowerCase(Locale.ROOT);
+            if (!ALLOWED_PROTOCOLS.contains(protocol)) {
+                throw new ServiceException("仅允许 HTTP/HTTPS 协议");
+            }
+            String host = url.getHost();
+            if (StringUtils.isEmpty(host)) {
+                throw new ServiceException("URL 主机非法");
+            }
+            InetAddress address = InetAddress.getByName(host);
+            if (address.isAnyLocalAddress()
+                    || address.isLoopbackAddress()
+                    || address.isLinkLocalAddress()
+                    || address.isSiteLocalAddress()
+                    || address.isMulticastAddress()) {
+                throw new ServiceException("禁止访问内网或本地地址");
+            }
+            String ip = address.getHostAddress();
+            if (ip.startsWith("169.254.") || "0.0.0.0".equals(ip) || "::1".equals(ip)) {
+                throw new ServiceException("禁止访问内网或本地地址");
+            }
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ServiceException("非法 URL: " + e.getMessage());
+        }
+    }
+}

+ 29 - 0
fs-common/src/main/java/com/fs/common/utils/security/XmlSecurityUtils.java

@@ -0,0 +1,29 @@
+package com.fs.common.utils.security;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * XML 解析安全工厂:禁用 DTD/外部实体,防止 XXE
+ */
+public final class XmlSecurityUtils {
+
+    private XmlSecurityUtils() {
+    }
+
+    /**
+     * 创建已禁用外部实体的 DocumentBuilder
+     */
+    public static DocumentBuilder newSafeDocumentBuilder() throws ParserConfigurationException {
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        // 禁止 DOCTYPE,阻断绝大多数 XXE
+        dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+        dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+        dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+        dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+        dbf.setXIncludeAware(false);
+        dbf.setExpandEntityReferences(false);
+        return dbf.newDocumentBuilder();
+    }
+}

+ 4 - 4
fs-company-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -12,6 +12,7 @@ import com.fs.common.config.FSConfig;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.file.FileUploadUtils;
 import com.fs.company.domain.CompanyUser;
@@ -221,10 +222,9 @@ public class CommonController extends AppBaseController {
 			throw new OssException("上传文件不能为空");
 		}
 		// 上传文件
-		String fileName = file.getOriginalFilename();
-		String suffix = fileName.substring(fileName.lastIndexOf("."));
-		CloudStorageService storage = OSSFactory.build();
-		String url = storage.uploadSuffix(file.getBytes(), suffix);
+		String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
 		return R.ok().put("url",url);
 	}
 

+ 3 - 0
fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -478,6 +478,9 @@ public class CompanyUserController extends AppBaseController {
         companyUser.setUserId(userId);
         companyUser.setVoicePrintUrl(param.getVoicePrintUrl());
 
+        // SSRF 防护:校验声纹 URL,禁止内网地址
+        com.fs.common.utils.security.UrlSecurityUtils.validatePublicHttpUrl(param.getVoicePrintUrl());
+
         //转换音频格式 mp3-wav
         String s = AudioUtils.audioWAVFromUrl(param.getVoicePrintUrl());
 

+ 32 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java

@@ -140,6 +140,7 @@ public class FsUserController extends AppBaseController {
     @ApiOperation("批量禁用会员")
     public ResponseResult<Boolean> disabledUser(@ApiParam(value = "联系人id集合", required = true) @RequestBody List<Long> userCompanyUserIds) {
         log.debug("批量禁用会员 ids: {}", JSON.toJSONString(userCompanyUserIds));
+        checkUserCompanyUserOwnership(userCompanyUserIds);
         Boolean r = userCompanyUserService.batchUpdateStatus(userCompanyUserIds, 2);
         return ResponseResult.ok(r);
     }
@@ -149,10 +150,27 @@ public class FsUserController extends AppBaseController {
     @ApiOperation("批量启用会员")
     public ResponseResult<Boolean> enabledUser(@ApiParam(value = "联系人id集合", required = true) @RequestBody List<Long> userCompanyUserIds) {
         log.debug("批量启用会员 ids: {}", JSON.toJSONString(userCompanyUserIds));
+        checkUserCompanyUserOwnership(userCompanyUserIds);
         Boolean r = userCompanyUserService.batchUpdateStatus(userCompanyUserIds, 1);
         return ResponseResult.ok(r);
     }
 
+    /**
+     * 校验联系人记录均归属当前销售,防止批量越权启停
+     */
+    private void checkUserCompanyUserOwnership(List<Long> userCompanyUserIds) {
+        if (userCompanyUserIds == null || userCompanyUserIds.isEmpty()) {
+            throw new ServiceException("参数不能为空");
+        }
+        Long currentUserId = Long.parseLong(getUserId());
+        for (Long id : userCompanyUserIds) {
+            FsUserCompanyUser rel = userCompanyUserService.selectFsUserCompanyUserById(id);
+            if (rel == null || rel.getCompanyUserId() == null || !rel.getCompanyUserId().equals(currentUserId)) {
+                throw new ServiceException("无权操作该会员");
+            }
+        }
+    }
+
     @Login
     @GetMapping("/firstPage/summaryCount")
     @ApiOperation("首页数据-顶部汇总统计")
@@ -268,6 +286,15 @@ public class FsUserController extends AppBaseController {
         if (Objects.isNull(fsUser)) {
             throw new ServiceException("用户不存在");
         }
+        // 校验会员是否归属当前销售,防止水平越权
+        Long currentUserId = Long.parseLong(getUserId());
+        FsUserCompanyUser query = new FsUserCompanyUser();
+        query.setUserId(param.getFsUserId());
+        query.setCompanyUserId(currentUserId);
+        List<FsUserCompanyUser> relations = userCompanyUserService.selectFsUserCompanyUserList(query);
+        if (relations == null || relations.isEmpty()) {
+            throw new ServiceException("无权操作该会员");
+        }
 
         fsUser.setNickName(param.getNickName());
         fsUser.setRemark(param.getRemark());
@@ -287,6 +314,11 @@ public class FsUserController extends AppBaseController {
         if (Objects.isNull(userCompanyUser)) {
             throw new ServiceException("用户不存在");
         }
+        // 校验归属当前销售
+        Long currentUserId = Long.parseLong(getUserId());
+        if (userCompanyUser.getCompanyUserId() == null || !userCompanyUser.getCompanyUserId().equals(currentUserId)) {
+            throw new ServiceException("无权操作该会员");
+        }
 
         userCompanyUser.setRemark(param.getRemark());
         userCompanyUserService.updateFsUserCompanyUser(userCompanyUser);

+ 17 - 0
fs-company-app/src/main/java/com/fs/app/controller/UserController.java

@@ -546,6 +546,14 @@ public class UserController extends AppBaseController {
     public R delMyCard(@RequestParam("cardId") Long cardId) {
         try {
             CompanyUserCard companyUserCard = companyUserCardService.selectCompanyUserCardById(cardId);
+            if (companyUserCard == null) {
+                return R.error("名片不存在");
+            }
+            // 校验名片归属,防止越权删除他人名片
+            Long currentUserId = Long.parseLong(getUserId());
+            if (companyUserCard.getUserId() == null || !companyUserCard.getUserId().equals(currentUserId)) {
+                return R.error("无权删除他人名片");
+            }
             String shortUrl = companyUserCard.getShortUrl();
             String[] chainArr = shortUrl.split(".cn/");
             String chain = "";
@@ -652,6 +660,7 @@ public class UserController extends AppBaseController {
         }
     }
 
+    @Login
     @GetMapping("/getUserList")
     public R getUserList() {
         CompanyUser companyUser = companyUserService.selectCompanyUserById(Long.parseLong(getUserId()));
@@ -666,10 +675,18 @@ public class UserController extends AppBaseController {
     @PostMapping("/changeUserState")
     public R changeUserState(@RequestParam Long userId) {
         log.debug("修改用户状态 userId :{}", userId);
+        CompanyUser operator = companyUserService.selectCompanyUserById(Long.parseLong(getUserId()));
+        if (Objects.isNull(operator) || !operator.isAdmin()) {
+            return R.error("仅管理员可操作");
+        }
         CompanyUser companyUser = companyUserService.selectCompanyUserById(userId);
         if (Objects.isNull(companyUser)) {
             throw new ServiceException("用户不存在");
         }
+        // 仅允许操作同公司用户
+        if (operator.getCompanyId() == null || !operator.getCompanyId().equals(companyUser.getCompanyId())) {
+            return R.error("无权操作其他公司用户");
+        }
 
         String state = "0".equals(companyUser.getStatus()) ? "1" : "0";
         companyUser.setStatus(state);

+ 1 - 1
fs-company-app/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 13 - 3
fs-company/src/main/java/com/fs/chat/controller/ChatDatasetFileController.java

@@ -168,10 +168,20 @@ public class ChatDatasetFileController extends BaseController
         int dotIndex = nameWithExtension.lastIndexOf(".");
 
         // 检查文件名是否有扩展名
+        String name;
         if (dotIndex > 0 && dotIndex < nameWithExtension.length() - 1) {
-            return nameWithExtension.substring(0, dotIndex);
+            name = nameWithExtension.substring(0, dotIndex);
+        } else {
+            name = nameWithExtension;
         }
-
-        return nameWithExtension;
+        // 文件名字符白名单,防止路径穿越/特殊字符注入(不影响正常中英文名)
+        name = name.replaceAll("[^a-zA-Z0-9_\\-\\u4e00-\\u9fa5]", "");
+        if (name.length() > 50) {
+            name = name.substring(0, 50);
+        }
+        if (name.isEmpty()) {
+            name = "file";
+        }
+        return name;
     }
 }

+ 3 - 3
fs-company/src/main/java/com/fs/chat/controller/ChatUploadController.java

@@ -10,6 +10,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.file.FileUploadUtils;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.company.domain.CompanyConfig;
 import com.fs.company.service.ICompanyConfigService;
 import com.fs.framework.security.LoginUser;
@@ -72,9 +73,8 @@ public class ChatUploadController extends BaseController
         fileDTO.setMedia_id(vo.getMedia_id());
         String url=weixinKfService.getFile(qwConfig.getCorpId(),qwConfig.getSecret(),fileDTO);
         vo.setUrl(url);
-        // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        // 上传文件(后缀白名单校验)
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         CloudStorageService storage = OSSFactory.build();
         String ossUrl = storage.uploadSuffix(file.getBytes(), suffix);
         return R.ok().put("data",vo).put("ossUrl",ossUrl);

+ 9 - 10
fs-company/src/main/java/com/fs/company/controller/common/CommonController.java

@@ -6,6 +6,7 @@ import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
@@ -229,8 +230,7 @@ public class CommonController
             throw new OssException("上传文件不能为空");
         }
         // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         CloudStorageService storage = OSSFactory.build();
         String url = storage.uploadSuffix(file.getBytes(), suffix);
         return R.ok().put("url",url);
@@ -244,11 +244,11 @@ public class CommonController
             throw new OssException("上传文件不能为空");
         }
         // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
-        String prefix = fileName.substring(0, fileName.lastIndexOf("."));
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
+        // 禁止使用原始文件名构造 OSS 路径,避免路径穿越
         CloudStorageService storage = OSSFactory.build();
-        String url = storage.upload(file.getBytes(), prefix+System.currentTimeMillis()+suffix);
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
+        String fileName = file.getOriginalFilename();
         return R.ok().put("url",url).put("fileName",fileName);
     }
 
@@ -314,10 +314,9 @@ public class CommonController
                 throw new OssException("上传文件不能为空");
             }
             // 上传文件
-            String fileName = file.getOriginalFilename();
-            String suffix = fileName.substring(fileName.lastIndexOf("."));
-            CloudStorageService storage = OSSFactory.build();
-            String url = storage.uploadSuffix(file.getBytes(), suffix);
+            String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
             vo.setErrno(0);
             List<WangUploadVO.WangUploadItem> items=new ArrayList<>();
             WangUploadVO.WangUploadItem item=new WangUploadVO.WangUploadItem();

+ 11 - 3
fs-company/src/main/java/com/fs/company/utils/AudioUtils.java

@@ -174,9 +174,17 @@ public class AudioUtils {
              // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
              process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
              */
-            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            // 方法2:ProcessBuilder 数组传参,避免 Runtime.exec 字符串拼接命令注入
+            // start 的第二个参数为空窗口标题,保持与原 cmd /c start 行为一致
+            ProcessBuilder pb = new ProcessBuilder(
+                    "cmd", "/c", "start", "",
+                    path + "silk_v3_encoder.exe",
+                    pcmPath,
+                    target,
+                    "-tencent"
+            );
+            process = pb.start();
+            process.waitFor();
             Thread.sleep(1000);
             // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {

+ 1 - 1
fs-company/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 4 - 4
fs-doctor-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -16,6 +16,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.common.utils.sign.Base64;
 import com.fs.common.utils.uuid.IdUtils;
 import com.fs.his.config.FsSysConfig;
@@ -168,10 +169,9 @@ public class CommonController {
 			throw new OssException("上传文件不能为空");
 		}
 		// 上传文件
-		String fileName = file.getOriginalFilename();
-		String suffix = fileName.substring(fileName.lastIndexOf("."));
-		CloudStorageService storage = OSSFactory.build();
-		String url = storage.uploadSuffix(file.getBytes(), suffix);
+		String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
 		return R.ok().put("url",url);
 	}
 

+ 1 - 1
fs-doctor-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-framework/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -27,7 +27,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 7 - 20
fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -112,14 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/baidu/**").anonymous()
                 .antMatchers("/baiduBack/**").anonymous()
                 .antMatchers("/test/gtp/*").anonymous()
-                .antMatchers("common/getTask/*").anonymous()
-                .antMatchers("//his/data/endFollow/*").anonymous()
-                .antMatchers("//his/data/end/*").anonymous()
-                .antMatchers("//his/data/addCF/*").anonymous()
-                .antMatchers("//his/data/addCom/*").anonymous()
-                .antMatchers("//his/data/testSendSub/*").anonymous()
-                .antMatchers("//his/data/test/*").anonymous()
-                .antMatchers("//his/data/Follow/*").anonymous()
+                // 安全修复:移除 his/data、uploadOSS、unbind 等危险匿名放行;支付/短信回调等仍需匿名
                 .antMatchers("/company/companyVoiceRobotic/callerResult").anonymous()
                 .antMatchers("/qw/data/*").anonymous()
                 .antMatchers("/app/common/expressNotify").anonymous()
@@ -132,23 +125,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/tzPay/*").anonymous()
                 .antMatchers("//his/pay/*").anonymous()
                 .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/chat/upload/uploadFile**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
-                .antMatchers("/common/unbindQwUserByServerIds").anonymous()
-                .antMatchers("/swagger-ui.html").anonymous()
-                .antMatchers("/swagger-resources/**").anonymous()
-                .antMatchers("/webjars/**").anonymous()
-                .antMatchers("/*/api-docs").anonymous()
-                .antMatchers("/druid/**").anonymous()
+                // swagger/druid 仅保留开发排查路径注释说明,生产应关闭;此处取消匿名避免监控台暴露
                 .antMatchers("/course/userVideo/videoTranscode").anonymous()
                 .antMatchers("/erp/call/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()
-                .headers().frameOptions().disable();
+                // 增加 nosniff,降低 MIME 嗅探导致的 XSS 风险;不影响正常业务响应
+                .headers()
+                    .contentTypeOptions()
+                    .and()
+                    .frameOptions().disable();
         httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
         // 添加JWT filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-live-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-mp-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 3 - 3
fs-qw-api/src/main/java/com/fs/app/controller/CommonController.java

@@ -2,6 +2,7 @@ package com.fs.app.controller;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.course.param.UserCourseComplaintRecordParam;
 import com.fs.course.service.IFsUserCourseComplaintTypeService;
 import com.fs.course.vo.FsUserCourseComplaintTypeListVO;
@@ -37,9 +38,8 @@ public class CommonController {
         {
             throw new OssException("上传文件不能为空");
         }
-        // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        // 上传文件(后缀白名单,不影响正常图片/文档/音视频上传)
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         CloudStorageService storage = OSSFactory.build();
         String url = storage.uploadSuffix(file.getBytes(), suffix);
         return R.ok().put("url",url);

+ 3 - 3
fs-qw-api/src/main/java/com/fs/app/controller/QwController.java

@@ -3,6 +3,7 @@ package com.fs.app.controller;
 import com.fs.app.service.QwDataCallbackService;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.common.utils.security.XmlSecurityUtils;
 import com.fs.qwApi.util.AesException;
 import com.fs.qwApi.util.WXBizMsgCrypt;
 import com.fs.qwApi.util.XMLParse;
@@ -13,7 +14,6 @@ import org.w3c.dom.Document;
 import org.xml.sax.InputSource;
 
 import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
 import java.io.StringReader;
 
 @RestController
@@ -102,8 +102,8 @@ public class QwController {
             // 加密成功
             // TODO: 解析出明文xml标签的内容进行处理
             // For example:
-            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-            DocumentBuilder db = dbf.newDocumentBuilder();
+            // 禁用 DTD/外部实体,防止 XXE(不影响正常企微明文 XML 解析)
+            DocumentBuilder db = XmlSecurityUtils.newSafeDocumentBuilder();
             StringReader sr = new StringReader(sMsg);
             InputSource is = new InputSource(sr);
             Document document = db.parse(is);

+ 1 - 1
fs-qw-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-qw-company-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-qw-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-qw-voice/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 3 - 2
fs-qwhook-msg/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -172,8 +172,9 @@ public class AudioUtils {
              process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
              */
             // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "start", "", path + "silk_v3_encoder.exe", pcmPath, target, "-tencent");
+            process = pb.start();
+            process.waitFor();
             Thread.sleep(1000);
             // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {

+ 1 - 1
fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 3 - 2
fs-qwhook-sop/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -173,8 +173,9 @@ public class AudioUtils {
              process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
              */
             // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "start", "", path + "silk_v3_encoder.exe", pcmPath, target, "-tencent");
+            process = pb.start();
+            process.waitFor();
             Thread.sleep(1000);
             // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {

+ 1 - 1
fs-qwhook-sop/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 3 - 2
fs-qwhook/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -172,8 +172,9 @@ public class AudioUtils {
              process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
              */
             // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "start", "", path + "silk_v3_encoder.exe", pcmPath, target, "-tencent");
+            process = pb.start();
+            process.waitFor();
             Thread.sleep(1000);
             // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {

+ 1 - 1
fs-qwhook/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-redis/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-repeat-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 6 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoServiceImpl.java

@@ -10,6 +10,7 @@ import java.util.stream.Collectors;
 
 import com.alibaba.fastjson.JSON;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.bean.BeanUtils;
@@ -478,10 +479,12 @@ public class FsUserVideoServiceImpl implements IFsUserVideoService {
         log.info("上传视频开始",uploadId);
         File tempFile = null;
         try {
-            // 将 MultipartFile 转换为临时文件
+            // 将 MultipartFile 转换为临时文件(固定 .tmp 后缀,禁止用户可控后缀落盘)
             String originalFilename = file.getOriginalFilename();
-            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
-            tempFile = File.createTempFile("upload_", suffix);
+            if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".mp4")) {
+                throw new ServiceException("仅支持 MP4 视频格式");
+            }
+            tempFile = File.createTempFile("upload_", ".tmp");
             file.transferTo(tempFile);
 
             // 点播空间名称

+ 12 - 7
fs-service/src/main/java/com/fs/course/service/impl/HuaweiObsServiceImpl.java

@@ -68,13 +68,18 @@ public class HuaweiObsServiceImpl implements IHuaweiObsService {
 
     private void mergeVideos(String fixedVideoPath, String uploadedVideoPath, String outputPath) throws IOException, InterruptedException {
 
-        String command = String.format("ffmpeg -i %s -i %s -filter_complex \"[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]\" -map \"[outv]\" -map \"[outa]\" %s", fixedVideoPath,uploadedVideoPath, outputPath);
-
-        // 打印调试信息
-        System.out.println("Executing command: " + command);
-
-        // 执行命令
-        Process process = Runtime.getRuntime().exec(command);
+        // 使用 ProcessBuilder 数组传参,避免 Runtime.exec 字符串拼接命令注入
+        ProcessBuilder processBuilder = new ProcessBuilder(
+                "ffmpeg",
+                "-i", fixedVideoPath,
+                "-i", uploadedVideoPath,
+                "-filter_complex", "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]",
+                "-map", "[outv]",
+                "-map", "[outa]",
+                outputPath
+        );
+        System.out.println("Executing command: " + processBuilder.command());
+        Process process = processBuilder.start();
 
         // 等待命令执行完成
         int exitCode = process.waitFor();

+ 9 - 7
fs-service/src/main/java/com/fs/course/service/impl/HuaweiVodServiceImpl.java

@@ -63,13 +63,15 @@ public class HuaweiVodServiceImpl implements IHuaweiVodService {
             writer.println("file '" + fixedVideoPath.replace("\\", "/") + "'");
             writer.println("file '" + uploadedVideoPath.replace("\\", "/") + "'");
         }
-        String command = String.format("ffmpeg -f concat -safe 0 -i %s -c copy %s", listFile.getPath(), outputPath);
-
-        // 打印调试信息
-        System.out.println("Executing command: " + command);
-
-        // 执行命令
-        Process process = Runtime.getRuntime().exec(command);
+        // 使用 ProcessBuilder 数组传参,避免 Runtime.exec 字符串拼接命令注入
+        ProcessBuilder processBuilder = new ProcessBuilder(
+                "ffmpeg", "-f", "concat", "-safe", "0",
+                "-i", listFile.getPath(),
+                "-c", "copy",
+                outputPath
+        );
+        System.out.println("Executing command: " + processBuilder.command());
+        Process process = processBuilder.start();
 
         // 等待命令执行完成
         int exitCode = process.waitFor();

+ 5 - 11
fs-service/src/main/java/com/fs/fastGpt/service/impl/FastGptCollectionServiceImpl.java

@@ -348,18 +348,12 @@ public class FastGptCollectionServiceImpl implements IFastGptCollectionService
      *  url转临时文件filr
      */
     public static File urlToFile(String fileUrl) throws Exception {
+        // SSRF 防护:协议白名单 + 拒绝内网地址
+        com.fs.common.utils.security.UrlSecurityUtils.validatePublicHttpUrl(fileUrl);
 
-        // 从URL中提取文件名,文件后缀
-        String fileExtension = getFileExtension(new File(fileUrl).getName());
-        String fileName = extractFileName(new URL(fileUrl))+new Date().getTime();
-        File tempFile=null;
-        if (!StringUtil.strIsNullOrEmpty(fileExtension)){
-            // 创建一个临时文件
-            tempFile = File.createTempFile(fileName, "."+fileExtension);
-        }else {
-            tempFile = File.createTempFile(fileName, null);
-        }
-
+        // 临时文件统一使用固定 .tmp 后缀,避免 URL 后缀可控
+        String fileName = "fastgpt_" + new Date().getTime();
+        File tempFile = File.createTempFile(fileName, ".tmp");
 
         // 使用 try-with-resources 语句自动关闭资源
         try (BufferedInputStream in = new BufferedInputStream(new URL(fileUrl).openStream());

+ 5 - 2
fs-service/src/main/java/com/fs/fastgptApi/util/AudioUtils.java

@@ -209,6 +209,8 @@ public class AudioUtils {
 
     public static String audioWAVFromUrl(String audioUrl) {
         try {
+            // SSRF 防护:禁止访问内网/本地地址
+            com.fs.common.utils.security.UrlSecurityUtils.validatePublicHttpUrl(audioUrl);
             // 下载文件到本地临时路径
             File tempFile = downloadFileFromUrl(audioUrl);
             if (tempFile == null) {
@@ -637,8 +639,9 @@ public class AudioUtils {
              process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
              */
             // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
-            process = Runtime.getRuntime().exec("cmd /c start  " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
-            process .waitFor();
+            ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "start", "", path + "silk_v3_encoder.exe", pcmPath, target, "-tencent");
+            process = pb.start();
+            process.waitFor();
             Thread.sleep(1000);
             // 有更好的方法会后续慢慢更新..
         } catch (Exception e) {

+ 8 - 0
fs-service/src/main/java/com/fs/his/param/FsStoreOrderParam.java

@@ -3,6 +3,7 @@ package com.fs.his.param;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.utils.security.SqlSafeUtils;
 import com.fs.his.vo.FsStoreOrderExportVO;
 import lombok.Data;
 
@@ -253,4 +254,11 @@ public class FsStoreOrderParam extends BaseEntity implements Serializable {
      * 商户号(merchant_app_config.merchant_id)
      */
     private String merchantId;
+
+    /**
+     * 排序方向白名单校验,防止 ${sortOrder} SQL 注入
+     */
+    public void setSortOrder(String sortOrder) {
+        this.sortOrder = SqlSafeUtils.validateOrderDirection(sortOrder);
+    }
 }

+ 8 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderParam.java

@@ -2,6 +2,7 @@ package com.fs.hisStore.param;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.utils.security.SqlSafeUtils;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -129,4 +130,11 @@ public class FsStoreOrderParam extends BaseEntity implements Serializable
 
     // 多个销售id
     private List<Long> companyUserIds;
+
+    /**
+     * 排序方向白名单校验,防止 ${sortOrder} SQL 注入
+     */
+    public void setSortOrder(String sortOrder) {
+        this.sortOrder = SqlSafeUtils.validateOrderDirection(sortOrder);
+    }
 }

+ 8 - 0
fs-service/src/main/java/com/fs/live/param/LiveOrderParam.java

@@ -2,6 +2,7 @@ package com.fs.live.param;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.utils.security.SqlSafeUtils;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -114,4 +115,11 @@ public class LiveOrderParam extends BaseEntity implements Serializable
     //银行交易流水号
     private String bankTransactionId;
 
+    /**
+     * 排序方向白名单校验,防止 ${sortOrder} SQL 注入
+     */
+    public void setSortOrder(String sortOrder) {
+        this.sortOrder = SqlSafeUtils.validateOrderDirection(sortOrder);
+    }
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -358,6 +358,8 @@ public class LiveDataServiceImpl implements ILiveDataService {
 
     @Override
     public TrendDataVO getCompanyTrendData(String type, String selectedDate, String category, Long companyId) {
+        // 白名单校验 category,防止 ${category} SQL 注入
+        category = com.fs.common.utils.security.SqlSafeUtils.validateLiveCategory(category);
         //DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
         LocalDate date = LocalDate.parse(selectedDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         DateRange currentRange = getDateRange(type, date, 0);
@@ -423,6 +425,8 @@ public class LiveDataServiceImpl implements ILiveDataService {
      */
     @Override
     public TrendDataVO getTrendData(String type, String selectedDate,String category) {
+        // 白名单校验 category,防止 ${category} SQL 注入
+        category = com.fs.common.utils.security.SqlSafeUtils.validateLiveCategory(category);
         //DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
         LocalDate date = LocalDate.parse(selectedDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         DateRange currentRange = getDateRange(type, date, 0);

+ 25 - 9
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -854,8 +854,8 @@ public class LiveServiceImpl implements ILiveService
             // 生成唯一的流密钥
             String streamKey = "stream_" + live.getLiveId() + "_" + System.currentTimeMillis();
 
-            // 构建FFmpeg推流命令
-            String ffmpegCmd = buildFFmpegCommand(curLiveVideo.getVideoUrl(), streamKey);
+            // 构建FFmpeg推流命令(参数列表,避免命令注入)
+            List<String> ffmpegCmd = buildFFmpegCommand(curLiveVideo.getVideoUrl(), streamKey);
 
             // 启动推流进程
             Process process = processManager.startProcess(ffmpegCmd);
@@ -1468,14 +1468,30 @@ public class LiveServiceImpl implements ILiveService
 
 
     /**
-     * 构建FFmpeg推流命令
+     * 构建FFmpeg推流命令(参数列表形式,路径作为独立参数)
      */
-    private String buildFFmpegCommand(String videoPath, String streamKey) {
-        return String.format(
-                "ffmpeg -re -stream_loop -1 -i \"%s\" -c:v libx264 -preset ultrafast -b:v 1000k " +
-                        "-c:a aac -b:a 128k -f flv rtmp://your-srs-server/live/%s",
-                videoPath, streamKey
-        );
+    private List<String> buildFFmpegCommand(String videoPath, String streamKey) {
+        List<String> cmd = new ArrayList<>();
+        cmd.add("ffmpeg");
+        cmd.add("-re");
+        cmd.add("-stream_loop");
+        cmd.add("-1");
+        cmd.add("-i");
+        cmd.add(videoPath);
+        cmd.add("-c:v");
+        cmd.add("libx264");
+        cmd.add("-preset");
+        cmd.add("ultrafast");
+        cmd.add("-b:v");
+        cmd.add("1000k");
+        cmd.add("-c:a");
+        cmd.add("aac");
+        cmd.add("-b:a");
+        cmd.add("128k");
+        cmd.add("-f");
+        cmd.add("flv");
+        cmd.add("rtmp://your-srs-server/live/" + streamKey);
+        return cmd;
     }
 
     /**

+ 4 - 3
fs-service/src/main/java/com/fs/live/utils/ProcessManager.java

@@ -4,6 +4,7 @@ import org.springframework.stereotype.Component;
 
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
@@ -18,10 +19,10 @@ public class ProcessManager {
     private final Map<String, Process> processMap = new ConcurrentHashMap<>();
 
     /**
-     * 启动进程
+     * 启动进程(命令参数列表,避免字符串 split 导致路径含空格时异常或注入)
      */
-    public Process startProcess(String command) throws IOException {
-        ProcessBuilder processBuilder = new ProcessBuilder(command.split(" "));
+    public Process startProcess(List<String> command) throws IOException {
+        ProcessBuilder processBuilder = new ProcessBuilder(command);
         Process process = processBuilder.start();
 
         String processId = getProcessId(process);

+ 7 - 0
fs-service/src/main/java/com/fs/qwApi/util/XMLParse.java

@@ -34,6 +34,13 @@ public  class XMLParse {
 		Object[] result = new Object[3];
 		try {
 			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+			// 禁用 DTD/外部实体,防止 XXE
+			dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+			dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+			dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+			dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+			dbf.setXIncludeAware(false);
+			dbf.setExpandEntityReferences(false);
 			DocumentBuilder db = dbf.newDocumentBuilder();
 			StringReader sr = new StringReader(xmltext);
 			InputSource is = new InputSource(sr);

+ 14 - 1
fs-service/src/main/java/com/fs/sms/service/impl/SmsTServiceImpl.java

@@ -21,6 +21,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.io.IOException;
@@ -32,10 +33,22 @@ public class SmsTServiceImpl implements SmsService {
     @Autowired
     private RedisCache redisCache;
 
+    /** 腾讯云 SecretId,从配置注入,禁止硬编码 */
+    @Value("${tencent.cloud.secret-id:}")
+    private String tencentSecretId;
+
+    /** 腾讯云 SecretKey,从配置注入,禁止硬编码 */
+    @Value("${tencent.cloud.secret-key:}")
+    private String tencentSecretKey;
+
     @Override
     public R sendTSms(String mobile, String code) {
         try {
-            Credential cred = new Credential("AKIDdAdvzoQpzuOo11mq2TntjKI5eqrhNp22", "zmLgpzzcwAFNJ4dx2iDTVHgw3ELCrIVg");
+            if (tencentSecretId == null || tencentSecretId.isEmpty()
+                    || tencentSecretKey == null || tencentSecretKey.isEmpty()) {
+                return R.error("腾讯云短信密钥未配置,请设置 tencent.cloud.secret-id/secret-key");
+            }
+            Credential cred = new Credential(tencentSecretId, tencentSecretKey);
             HttpProfile httpProfile = new HttpProfile();
             httpProfile.setEndpoint("sms.tencentcloudapi.com");
             ClientProfile clientProfile = new ClientProfile();

+ 1 - 1
fs-store/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 5 - 6
fs-store/src/main/java/com/fs/store/controller/common/CommonController.java

@@ -6,6 +6,7 @@ import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.file.FileUploadUtils;
 import com.fs.common.utils.file.FileUtils;
@@ -133,8 +134,7 @@ public class CommonController
             throw new OssException("上传文件不能为空");
         }
         // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         CloudStorageService storage = OSSFactory.build();
         String url = storage.uploadSuffix(file.getBytes(), suffix);
         return R.ok().put("url",url);
@@ -150,10 +150,9 @@ public class CommonController
                 throw new OssException("上传文件不能为空");
             }
             // 上传文件
-            String fileName = file.getOriginalFilename();
-            String suffix = fileName.substring(fileName.lastIndexOf("."));
-            CloudStorageService storage = OSSFactory.build();
-            String url = storage.uploadSuffix(file.getBytes(), suffix);
+            String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
             vo.setErrno(0);
             List<WangUploadVO.WangUploadItem> items=new ArrayList<>();
             WangUploadVO.WangUploadItem item=new WangUploadVO.WangUploadItem();

+ 1 - 1
fs-user-app-ai-chat/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 38 - 24
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -58,7 +58,11 @@ import static com.fs.his.utils.PhoneUtil.encryptPhoneOldKey;
 @Slf4j
 public class AppLoginController extends AppBaseController{
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
-    private static final String INTERNAL_LOGIN_CODE = "EdisonX!@#.+-123";
+    /**
+     * 原硬编码万能验证码已废弃(存在任意用户登录风险)。
+     * 如需内部联调,请走正常短信验证或专用开发环境配置,禁止在生产使用固定验证码。
+     */
+    private static final String INTERNAL_LOGIN_CODE = "";
     private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
     @Autowired
     private IFsUserService userService;
@@ -197,13 +201,16 @@ public class AppLoginController extends AppBaseController{
         if (!CollectionUtil.isEmpty(users)){
             return R.error("此账号已经注册");
         }
-        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        String smsCodeKey = "sms:code:" + phone;
+        String redisCode = redisCache.getCacheObject(smsCodeKey);
         if (StringUtils.isEmpty(redisCode)){
             return R.error("验证码已过期,请重新发送");
         }
         if (!redisCode.equals(code)) {
             return R.error("验证码错误");
         }
+        // 验证成功后立即删除验证码,防止重复使用
+        redisCache.deleteObject(smsCodeKey);
         FsUser user = new FsUser();
         // 创建新用户
         user.setPhone(phone);
@@ -501,15 +508,19 @@ public class AppLoginController extends AppBaseController{
             //如果出现了一个手机号多个用户的情况,找出登陆过app的那个用户
             user.removeIf(fsUser -> StringUtils.isEmpty(fsUser.getHistoryApp()));
         }
-        boolean isInternalLogin = INTERNAL_LOGIN_CODE.equals(code);
+        // 万能验证码已清空禁用;仅当配置了非空内部码时才生效
+        boolean isInternalLogin = StringUtils.isNotEmpty(INTERNAL_LOGIN_CODE) && INTERNAL_LOGIN_CODE.equals(code);
         if (!isInternalLogin) {
-            String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+            String smsCodeKey = "sms:code:" + phone;
+            String redisCode = redisCache.getCacheObject(smsCodeKey);
             if (StringUtils.isEmpty(redisCode)){
                 return R.error("验证码已过期,请重新发送");
             }
             if (!redisCode.equals(code)) {
                 return R.error("验证码错误");
             }
+            // 验证成功后立即删除验证码,防止重复使用
+            redisCache.deleteObject(smsCodeKey);
         }
         FsUser currentUser;
         if (CollectionUtil.isEmpty(user)) {
@@ -539,13 +550,24 @@ public class AppLoginController extends AppBaseController{
         if (CollectionUtil.isEmpty(user)){
             return R.error("此电话号码未绑定用户");
         }
-        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        String smsCodeKey = "sms:code:" + phone;
+        String failKey = "sms:fail:" + phone;
+        Integer failCount = redisCache.getCacheObject(failKey);
+        if (failCount != null && failCount >= 5) {
+            return R.error("验证码错误次数过多,请30分钟后再试");
+        }
+        String redisCode = redisCache.getCacheObject(smsCodeKey);
         if (StringUtils.isEmpty(redisCode)){
             return R.error("验证码已过期,请重新发送");
         }
         if (!redisCode.equals(code)) {
+            int nextFail = failCount == null ? 1 : failCount + 1;
+            redisCache.setCacheObject(failKey, nextFail, 30, TimeUnit.MINUTES);
             return R.error("验证码错误");
         }
+        // 验证成功后立即失效验证码,防止重复使用
+        redisCache.deleteObject(smsCodeKey);
+        redisCache.deleteObject(failKey);
         String password = Md5Utils.hash(newPassword);
         return userService.updatePasswordByPhone(password,encryptPhone);
     }
@@ -949,16 +971,14 @@ public class AppLoginController extends AppBaseController{
         return generateTokenAndReturn(user);
     }
 
+    /**
+     * 安全修复:原接口仅凭 userId 即可颁发 token,属于登录后门,已禁用。
+     * 保留路径避免前端 404 误判,统一返回无权限。
+     */
     @PostMapping("/loginByUserId")
     public R loginByUserId(@RequestBody FsUserLoginByWeChatParam param){
-        FsUser user = userService.selectFsUserByUserId(param.getUserId());
-        if (user == null){
-            return R.error("用户不存在");
-        }
-        if (StringUtils.isNotEmpty(param.getJpushId())){
-            updateExistingUserJpushId(user, param.getJpushId());
-        }
-        return generateTokenAndReturn(user);
+        logger.warn("已禁用的高危接口被调用: /loginByUserId, userId={}", param != null ? param.getUserId() : null);
+        return R.error(403, "该接口已禁用");
     }
 
     private FsUser createNewUser(FsUserLoginParam param) {
@@ -1006,19 +1026,13 @@ public class AppLoginController extends AppBaseController{
         return user;
     }
 
+    /**
+     * 安全修复:测试后门接口已禁用,禁止仅凭 userId 颁发 token
+     */
     @PostMapping("/loginTest")
     public R loginTest(@RequestBody String userId) {
-        FsUser user = userMapper.selectFsUserByUserId(Long.parseLong(userId));
-        if (user!=null){
-            String token = jwtUtils.generateToken(user.getUserId());
-            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
-            Map<String, Object> map = new HashMap<>();
-            map.put("token", token);
-            map.put("user", user);
-            bindCompanyUserInfoByPhone(user);
-            return R.ok(map);
-        }
-        return R.error("用户不存在!");
+        logger.warn("已禁用的高危接口被调用: /loginTest, userId={}", userId);
+        return R.error(403, "该接口已禁用");
     }
 
     /**

+ 4 - 4
fs-user-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -26,6 +26,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.common.utils.file.FileUploadUtils;
 import com.fs.common.utils.http.HttpUtils;
 import com.fs.common.utils.sign.Md5Utils;
@@ -355,10 +356,9 @@ public class CommonController {
 			throw new OssException("上传文件不能为空");
 		}
 		// 上传文件
-		String fileName = file.getOriginalFilename();
-		String suffix = fileName.substring(fileName.lastIndexOf("."));
-		CloudStorageService storage = OSSFactory.build();
-		String url = storage.uploadSuffix(file.getBytes(), suffix);
+		String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
 		return R.ok().put("url",url);
 	}
 

+ 15 - 7
fs-user-app/src/main/java/com/fs/app/controller/TalentController.java

@@ -161,7 +161,7 @@ public class TalentController extends  AppBaseController{
      * @return
      * @throws Exception
      */
-    //@Login
+    @Login
     @PostMapping("/uploadOSSTalent")
     public R uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
         //校验文件是否为空
@@ -169,21 +169,29 @@ public class TalentController extends  AppBaseController{
             throw new OssException("上传文件不能为空");
         }
 
-        //获取文件基本信息
+        // 后缀白名单 + 路径穿越字符过滤
+        String suffix = com.fs.common.utils.security.OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         String originalFilename = file.getOriginalFilename();
-        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
         String fileType = file.getContentType();
 
         //如果是视频文件且需要缩略图
         if (fileType != null && fileType.startsWith("video/")) {
             // 3.1 校验视频格式(示例仅允许MP4)
-            if (!fileType.equals("video/mp4")) {
+            if (!".mp4".equalsIgnoreCase(suffix) && !fileType.equals("video/mp4")) {
                 return R.error("仅支持MP4视频格式");
             }
 
-            //保存临时视频文件
-            String videoFileName = System.currentTimeMillis() + "_" + originalFilename;
-            File videoFile = new File(VIDEO_UPLOAD_DIR, videoFileName);
+            // UUID 重命名,禁止使用原始文件名拼接本地路径(防路径穿越)
+            String videoFileName = UUID.randomUUID().toString().replace("-", "") + ".mp4";
+            File uploadDir = new File(VIDEO_UPLOAD_DIR);
+            if (!uploadDir.exists()) {
+                uploadDir.mkdirs();
+            }
+            File videoFile = new File(uploadDir, videoFileName);
+            // 二次确认落盘路径仍在约定目录内
+            if (!videoFile.getCanonicalPath().startsWith(uploadDir.getCanonicalPath())) {
+                throw new OssException("文件路径非法");
+            }
             file.transferTo(videoFile);
 
             //获取视频元信息(宽高、大小、时长等)

+ 11 - 1
fs-user-app/src/main/java/com/fs/app/controller/WxPayController.java

@@ -87,25 +87,35 @@ public class WxPayController {
                 String outtradeno = result.getOutTradeNo();
                 String tradeNo = result.getTransactionId();
                 String[] orderId=outtradeno.split("-");
+                // 每个 case 必须 break,避免 fall-through 导致一笔支付重复入账多个业务线
                 switch (orderId[0]){
                     case "inquiry":
                         inquiryOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
+                        break;
                     case "store":
                         storeOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
+                        break;
                     case "package":
                         packageOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
+                        break;
                     case "course":
                         courseOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
+                        break;
                     case "appvip":
                         userVipOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
+                        break;
                     case "integral":
                         integralOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
                         break;
                     case "product":
                         courseProductOrderService.payConfirm("",orderId[1],tradeNo,"",1,tradeNo,null);
+                        break;
                     case "payment":
                         paymentService.payConfirm(orderId[1], outtradeno,tradeNo,null);
-
+                        break;
+                    default:
+                        logger.warn("未知订单前缀: {}", orderId[0]);
+                        break;
                 }
                 return WxPayNotifyResponse.success("处理成功!");
             }else{

+ 3 - 3
fs-user-app/src/main/java/com/fs/app/controller/store/CommonScrmController.java

@@ -11,6 +11,7 @@ import com.fs.app.utils.JwtUtils;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.security.OssUploadSecurityUtils;
 import com.fs.company.service.ICompanyMoneyLogsService;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
@@ -362,9 +363,8 @@ public class CommonScrmController extends AppBaseController {
         if (file.isEmpty()) {
             throw new OssException("上传文件不能为空");
         }
-        // 上传文件
-        String fileName = file.getOriginalFilename();
-        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        // 上传文件(后缀白名单,不影响正常图片/文档/音视频上传)
+        String suffix = OssUploadSecurityUtils.validateAndGetDotSuffix(file);
         CloudStorageService storage = OSSFactory.build();
         String url = storage.uploadSuffix(file.getBytes(), suffix);
         return R.ok().put("url", url);

+ 22 - 1
fs-user-app/src/main/java/com/fs/app/controller/store/PayScrmController.java

@@ -2,6 +2,7 @@ package com.fs.app.controller.store;
 
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.redis.RedisCache;
@@ -10,7 +11,11 @@ import com.fs.company.service.ICompanyUserService;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.hisStore.service.*;
 import com.fs.huifuPay.domain.HuiFuResult;
+import com.fs.huifuPay.sdk.opps.core.BasePay;
+import com.fs.huifuPay.sdk.opps.core.sign.JsonUtils;
 import com.fs.huifuPay.sdk.opps.core.utils.HuiFuUtils;
+import com.fs.huifuPay.sdk.opps.core.utils.RsaUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.live.service.ILiveOrderService;
 import com.fs.ybPay.domain.OrderCallback;
 import com.fs.ybPay.domain.OrderResult;
@@ -74,7 +79,23 @@ public class PayScrmController {
     @ApiOperation("汇付第三方支付回调")
     @PostMapping(value="/hfPayNotify")
     public String hfPayNotify(@RequestParam String resp_desc, @RequestParam String resp_code, @RequestParam String sign, @RequestParam String resp_data) throws Exception {
-        //验证
+        // 先验签,防止伪造支付成功回调
+        if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(resp_data)) {
+            logger.error("汇付回调缺少签名或业务数据");
+            return "fail";
+        }
+        try {
+            JSONObject data = JSON.parseObject(resp_data);
+            String sortedData = JsonUtils.sort4JsonString(JSONObject.toJSONString(data), 5);
+            String publicKey = BasePay.HUIFU_DEFAULT_PUBLIC_KEY;
+            if (!RsaUtils.verify(sortedData, publicKey, sign)) {
+                logger.error("汇付回调验签失败, resp_data={}", resp_data);
+                return "fail";
+            }
+        } catch (Exception e) {
+            logger.error("汇付回调验签异常", e);
+            return "fail";
+        }
         HuiFuResult o = JSON.parseObject(resp_data, HuiFuResult.class);
         logger.info("汇付支付回调:"+o);
         if(o.getResp_code().equals("00000000") && o.getNotify_type().equals("1")){

+ 1 - 1
fs-user-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-watch/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)

+ 1 - 1
fs-wx-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -28,7 +28,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 
     static
     {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 安全修复:关闭 AutoType,防止反序列化 RCE
     }
 
     public FastJson2JsonRedisSerializer(Class<T> clazz)