IoT DC3 安全审计报告

23次阅读
没有评论

IoT DC3 安全审计报告

审计对象:分布式工业 AI 物联网平台 IoT DC3(Java 21 + Spring Boot/Spring Cloud WebFlux + Vue3 前端)
审计范围:后端 Java(约 200,657 行 / 1,844 文件)+ 前端 Vue(dc3-web)+ 部署编排(compose / initdb SQL / env 模板)
审计方法:按漏洞类别并行排查 + 逐点人工核实调用链(已排除 target/ 编译产物与 node_modules)
审计日期:2026-08-13


严重程度统计

严重程度 数量 编号
🔴 高危 7 H-1 ~ H-7
🟠 中危 5 M-1 ~ M-5
🟡 低危 / 加固建议 9 L-1 ~ L-9

核心结论:平台在业务控制器层的租户隔离与 RBAC 覆盖较扎实(绝大多数 getById/delete/update/list 有租户拦截器或 requireTenant 兜底);反序列化、命令执行/RCE 面防护良好(无 Java 原生反序列化、无 Jackson default typing、无 SpEL/模板注入、唯一 OS 命令执行点有白名单)。真正的严重问题集中在 ① 部署层默认密钥/口令公开(可伪造 Token、弱口令登入)② SSRF 系统性缺失防护 ③ 分页 ORDER BY SQL 注入 ④ 对 X-Auth-Principal 信任头的信任边界。


🔴 高危漏洞

H-1. JWT 签名密钥默认值公开 → 任意 Token 伪造 → 完全接管系统

  • 位置:.env.example:17、dc3/docker-compose.yml:27(默认值);dc3-common/dc3-common-public/src/main/java/io/github/pnoker/common/utils/KeyUtil.java:236-270(消费)

  • 类型:硬编码密钥 / 鉴权绕过

  • 漏洞描述:
    登录态令牌 X-Auth-Token 由 KeyUtil.generateToken 用 DC3_SECURITY_KEY 做 HS256 签名(Keys.hmacShaKeyFor(securityKey),且 issuer/subject 也拼入 securityKey)。该密钥在部署模板中以公开默认值提供:

    # .env.example:17
    DC3_SECURITY_KEY=dc3.security.key.2026.io.github.pnoker
    # docker-compose.yml:27
    DC3_SECURITY_KEY: ${DC3_SECURITY_KEY:-dc3.security.key.2026.io.github.pnoker}
    

    KeyUtil.getSecurityKey() 仅校验"已配置",对任何环境(含生产)都不做弱密钥 fail-fast(对比 HmacAuthConfig 对 HMAC 密钥的 fail-fast)。

  • 可利用性:攻击者从开源仓库获知默认密钥,可离线伪造任意 (principalId, tenantId) 的合法 Token。默认管理员为 tenantId=1、principalId=1,构造 X-Auth-Tenant: default + X-Auth-Token: <伪造令牌> 即可通过网关校验,以持有通配符 * 权限的超级管理员身份接管全部租户。即使生产环境没有默认部署,任何复用该默认值的部署同样中招。

  • 修复建议:

    1. 移除 .env.example / compose 中的默认值,改为必填项或部署时随机生成;
    2. KeyUtil.getSecurityKey() 对已知默认值 / 弱密钥做 fail-fast(效仿 HmacAuthConfig.validateSecret);
    3. 生产部署必须注入独立强随机密钥并纳入密钥管理。

H-2. 分页列表通用 ORDER BY SQL 注入(盲注拖库)

  • 位置:dc3-common/dc3-common-public/src/main/java/io/github/pnoker/common/entity/common/Pages.java:63 → dc3-common/dc3-common-public/src/main/java/io/github/pnoker/common/utils/PageUtil.java:70-80 → dc3-common/dc3-common-postgres/src/main/java/io/github/pnoker/common/config/MybatisPlusConfig.java:73

  • 类型:SQL 注入

  • 漏洞描述:所有分页列表接口的查询参数都含 Pages,其 orders 字段带 @Setter,可被请求体 JSON 直接绑定:

    // Pages.java:63 —— 无任何校验/@JsonIgnore
    private List<OrderItem> orders = new ArrayList<>(2);
    

    PageUtil.page() 对 column 只过滤空值、无白名单即透传;MybatisPlusConfig 的 PaginationInnerInterceptor 未开启 injectionDetection(MyBatis-Plus 默认 false),OrderItem.column 被原样拼入 ORDER BY <column>。该模式经 PageUtil.page() 复用于 auth/manager/data/dal/agentic 全部模块约 40+ 处。

  • 可利用性:任意持有 xx:list 权限的租户用户,向任一 POST .../list 接口提交:

    {"page":{"orders":[{"column":"(SELECT CASE WHEN substring(current_database(),1,1)='d' THEN 1 ELSE 1/(SELECT 0) END)","asc":true}]}}
    

    PostgreSQL 允许 ORDER BY 标量子查询 → 时间盲注(pg_sleep(5))/ 布尔盲注逐字符拖库。租户行级拦截器只改写外层 WHERE,不会给 ORDER BY 内子查询加租户过滤,存在跨租户读取 manager 库用户/凭据/角色等敏感表的潜力。

  • 修复建议:

    1. 在 PageUtil.page() 或反序列化层对 OrderItem.column 白名单校验(如 ^[A-Za-z0-9_]+$,或按实体字段映射);
    2. 开启 interceptor.setInjectionDetection(true)(MyBatis-Plus 3.5.17 支持);
    3. 服务端忽略客户端 orders,统一按安全字段排序。

H-3. 系统性 SSRF —— HTTP 驱动 baseUrl/path 可控(可读可写、响应回显)

  • 位置:出站 dc3-driver/dc3-driver-http/src/main/java/io/github/pnoker/driver/service/impl/HttpDriverCustomServiceImpl.java:203-224;写入端 dc3-common/dc3-common-manager/src/main/java/io/github/pnoker/common/manager/controller/DriverAttributeConfigController.java:86-103,135-153、PointAttributeConfigController.java、DeviceController.java:272-304
  • 类型:SSRF
  • 漏洞描述:HTTP 驱动的设备属性配置 baseUrl 与点位属性配置 path 仅 @NotNull 校验(configValue 无内容校验),直接进 WebClient.builder().baseUrl(...).uri(path)。path 甚至可填绝对 URL 覆盖 baseUrl。
  • 可利用性:持 driver_attribute_config:add/update 权限(种子管理员持 * 通配)→ 把 baseUrl/path 设为 http://169.254.169.254/latest/meta-data/、http://127.0.0.1:8080/、内网地址 → 触发点位读取/写入 → 驱动向目标发起请求,响应体作为点位值落库,攻击者再读点位值即可回显内网响应。完全双向可读可写 SSRF。
  • 修复建议:属性写入侧按属性 code 校验 baseUrl/path/host/port(仅 http/https、拒绝私网/回环/链路本地网段);驱动出站侧解析 DNS 后再次校验(防 DNS rebinding)、禁用/校验重定向;网络层 ACL 收窄驱动节点出网。

H-4. 系统性 SSRF —— 13+ 个协议驱动出站目标无任何校验

  • 位置(host/port/url 均来自设备配置,出站直接连接):
    • dc3-driver-tcp-udp/.../TcpUdpDriverCustomServiceImpl.java:292-337(new Socket()/DatagramSocket)
    • dc3-driver-coap/.../CoapDriverCustomServiceImpl.java:108-115(coap://host:port)
    • dc3-driver-modbus-tcp/.../ModbusTcpDriverCustomServiceImpl.java:291(createTcpMaster)
    • dc3-driver-opc-ua/.../OpcUaDriverCustomServiceImpl.java:235(opc.tcp://%s:%s%s)
    • dc3-driver-snmp/.../SnmpDriverCustomServiceImpl.java:249-258(udp:%s/%d)
    • dc3-driver-plcs7/.../PlcS7DriverCustomServiceImpl.java:192、dc3-driver-melsec/.../MelsecDriverCustomServiceImpl.java:188、dc3-driver-fins/...:218-219、dc3-driver-ethernet-ip/...:206
    • dc3-common-sql/.../AbstractJdbcDriverCustomService.java:182-213(MySQL/PG/Oracle/SQLServer JDBC)
    • dc3-driver-opc-da(DCOM/RPC)
  • 类型:SSRF
  • 漏洞描述:全部主动连接型驱动把 driverConfig 的 host/ip/port/database/url 直接用于出站连接。各驱动 validate() 仅 checkRequired(字段存在性)。全库 grep 无任何 私网 IP 黑名单、scheme 校验、DNS rebinding 防护。
  • 可利用性:持配置权限把设备 host/port 改为内网目标(127.0.0.1:3306、169.254.169.254、内网 Redis/ES/管理端口)→ 触发读取/调度采集 → 驱动以 TCP/UDP/JDBC/CoAP/Modbus 等协议向内网发起连接;响应差异(端口连通性、错误信息、超时)作为点位值/健康状态回显,形成内网端口/服务探测 oracle;JDBC 变体可直接命中内网数据库指纹。
  • 修复建议:在 DriverMetadata/DeviceMetadata 装载或驱动 getConnector 处统一校验出站目标(拒绝私网/链路本地/元数据网段、防 DNS rebinding);协议无关 host allowlist 按租户配置;驱动节点出网策略收窄。

H-5. 默认超级管理员账号(dc3/dc3dc3dc3,通配符全权限)

  • 位置:dc3/dependencies/postgres/initdb/02-iot-dc3-auth.sql:1860-1912(凭据 1868-1875,通配角色 2082-2083)

  • 类型:硬编码凭据 / 未授权访问

  • 漏洞描述:数据库初始化 SQL 直接播种超级管理员:

    -- raw password: dc3dc3dc3
    INSERT INTO dc3_local_credential VALUES (1, 1, 'dc3', 'dc3', 'PASSWORD', '$2b$12$cSuC2gIZqrti2JLHur5JU...', 'BCRYPT', ...);
    

    明文口令写在注释里,bcrypt 哈希提交在代码中,require_password_change=0(无强制改密),并绑定 role_code='admin' → 通配符资源 20000(* 全权限)。

  • 可利用性:使用仓库自带 initdb 脚本初始化(含生产 compose 一键部署)的环境默认存在 dc3/dc3dc3dc3 全权限管理员,任何知道该默认值的人可 POST /api/v3/auth/token/generate 直接登录,拥有全部租户/设备/驱动管理能力。

  • 修复建议:initdb 移除固定口令账号,改为首启引导创建管理员并随机生成口令;或强制 require_password_change=1 一次性激活。


H-6. X-Auth-Principal 信任头伪造(HMAC 弱默认 / 关闭时完全信任)

  • 位置:dc3-common/dc3-common-web/src/main/java/io/github/pnoker/common/security/GatewayJwtConverter.java:73-81、dc3-common/dc3-common-public/src/main/java/io/github/pnoker/common/utils/HmacAuthSigner.java:57-71、.env.example:18、dc3/docker-compose.yml:28
  • 类型:鉴权绕过
  • 漏洞描述:下游中心信任 X-Auth-Principal 头,仅当 HMAC 启用时校验 X-Auth-Sign。AUTH_HMAC_SECRET 在部署模板中默认 io.github.pnoker.dc3(公开弱密钥),secret 留空时 HMAC 直接关闭、头部原样信任。WebFluxSecurityConfig 的 anyExchange().authenticated() 只验证"头部存在且可解析",不验证真实身份。
  • 可利用性:攻击者只要能直连任一中心端口(auth 8300 被发布 / 经内网 SSRF),用公开弱密钥计算 X-Auth-Sign = HMAC(secret, principalJson) 伪造头部即通过校验,以任意主体身份执行接口;HMAC 关闭时甚至无需签名。
  • 修复建议:所有环境强制设置强 HMAC 密钥(当前 pre/pro 已 fail-fast,dev/test 无保护);中心端口不得直接暴露,对外只开网关 8000。

H-7. OAuth 动态客户端注册信任客户端提供的 X-Auth-Principal 头

  • 位置:dc3-common/dc3-common-auth/src/main/java/io/github/pnoker/common/auth/controller/OAuthController.java:120-135,253-258、dc3-common/dc3-common-auth/src/main/java/io/github/pnoker/common/auth/biz/impl/OAuthMcpRuntimeServiceImpl.java:263-327

  • 类型:鉴权绕过 / 越权

  • 漏洞描述(已亲自核实):/oauth2/register 标注 @PublicEndpoint(网关路由亦无认证过滤器),直接读取可选的 X-Auth-Principal 头并 parsePrincipal 交给 registerClient;requireAuthenticatedPrincipal 仅校验 principalId/tenantId 非空,不校验头是否来自可信网关/HMAC:

    // OAuthController.java:120-135(节选)
    @PublicEndpoint
    @PostMapping(value = McpConstant.OAUTH2_REGISTER, ...)
    public Mono<ResponseEntity<?>> register(
            @RequestBody OAuthClientRegistrationRequestVO request,
            @RequestHeader(value = RequestConstant.Header.X_AUTH_PRINCIPAL, required = false) String principalJson) {
        ... oauthMcpRuntimeService.registerClient(bo, parsePrincipal(principalJson));
    

    注册后的 ownerPrincipalId/tenantId 全部取自该伪造头(请求体 tenant 反而被忽略——但攻击者控制的是头)。

  • 可利用性:未认证攻击者伪造 X-Auth-Principal 头,POST /oauth2/register 注册归属任意租户/主体的 CONFIDENTIAL 客户端(获得 client_secret),可结合 client_credentials 授权指定 service account,作为跨租户服务账号令牌铸造的前置条件,并污染 OAuth 注册表。

  • 修复建议:公共 OAuth 端点不信任客户端直接提供的 X-Auth-Principal 头,改为从已认证 Spring Security 上下文取主体;强制 HMAC 校验该头;关闭非受保护环境的 DCR。


🟠 中危漏洞

M-1. MQTT 客户端私钥 + 加密口令提交到 git

  • 位置:dc3-common/dc3-common-mqtt/src/main/resources/certs/client.key(git 跟踪的 ENCRYPTED RSA 私钥)、application-mqtt.yml:27、MqttProperties.java:65(口令 dc3-client)
  • 类型:硬编码凭据 / 证书私钥泄露
  • 描述:MQTT X509 客户端私钥与加密口令均提交进 main 资源。攻击者可解出私钥,伪装受信 MQTT 客户端连接 broker(若校验 X509),收发设备命令与数据。
  • 修复:移除私钥与固定口令,证书部署侧注入;口令环境变量注入。

M-2. PrincipalController 跨租户越权(IDOR)

  • 位置:dc3-common/dc3-common-auth/src/main/java/io/github/pnoker/common/auth/controller/PrincipalController.java:89-134,151-174;dc3_principal 在 TenantLineHandlerImpl.java:43-54 的 IGNORE_TABLES 白名单中(无 tenant_id 列)
  • 类型:越权(IDOR) / 租户隔离缺失
  • 描述:getById/list/enable/disable 均未做租户归属校验,仅靠 RBAC。持 principal:* 权限者可跨租户枚举全部用户身份并 disable 任意 principal(含其他租户管理员),造成跨租户可用性破坏。
  • 修复:身份类接口增加租户归属校验(经 dc3_tenant_membership/dc3_user.tenant_id 反向确认),enable/disable 至少校验 isTenantMember。

M-3. TokenController cancel/check 跨租户

  • 位置:dc3-common/dc3-common-auth/src/main/java/io/github/pnoker/common/auth/controller/TokenController.java:172-183,200-217
  • 类型:越权 / 租户隔离缺失
  • 描述:/token/cancel、/token/check 用请求体中的 name/tenant 标识目标而非绑定当前调用者。持 token:delete 者可注销任意租户任意用户的有效会话;持 token:get 可跨租户探测令牌有效性。
  • 修复:以当前认证主体与租户为强制边界,忽略/校验请求体 name/tenant 必须与调用者一致(管理员例外需显式授权)。

M-4. Agentic LLM 供应商 baseUrl SSRF

  • 位置:dc3-common/dc3-common-agentic/src/main/java/io/github/pnoker/common/agentic/config/ChatClientFactory.java:301-322、service/runtime/OpenAiCompatibleAgenticRuntime.java:494-499;写入 service/impl/ModelProviderServiceImpl.java:110-117(validate 仅判 baseUrl 非空)、controller/ProviderController.java:88,111
  • 类型:SSRF
  • 描述:模型供应商 baseUrl 仅判非空,OkHttp 出站到可控地址。持 provider:add/update 者配置 http://169.254.169.254/ 或内网地址,触发模型调用即向目标发起请求,可内网探测/回显。
  • 修复:validate 增加 scheme(仅 https/白名单域名)、解析后私网/元数据网段校验;LLM baseUrl 显式域名 allowlist。

M-5. 内部 gRPC 无鉴权 + PLAINTEXT 传输

  • 位置:dc3-common/dc3-common-auth/src/main/java/io/github/pnoker/common/auth/grpc/UserServer.java:52-59、dc3-common/dc3-common-facade/dc3-common-facade-grpc/src/main/java/io/github/pnoker/common/facade/grpc/config/RequestIdGrpcServerInterceptor.java、dc3-common/dc3-common-gateway/src/main/resources/application-gateway.yml:34
  • 类型:未授权访问 / 租户隔离缺失
  • 描述:gRPC 服务仅有 RequestIdGrpcServerInterceptor(无认证/鉴权),传输 PLAINTEXT,tenant 由调用方自报。能触达 gRPC 端口者(被攻陷 driver、SSRF 目标)可跨租户读取用户/租户/凭据,或以任意 tenantId 调用 Token/Permission 等特权门面。
  • 修复:gRPC 启用 mTLS 或共享令牌鉴权拦截器;服务端校验请求中 tenantId 来源;gRPC 端口不对外暴露。

🟡 低危 / 加固建议

编号 位置 类型 说明
L-1 dc3-web/src/utils/notificationUtil.ts:37,65,93 + src/config/axios/index.ts:136 XSS/HTML注入 全局通知 ElNotification({dangerouslyUseHTMLString:true}),axios 拦截器把后端返回的 error code 作为 title 以 HTML 渲染。当前受 CSP 限制难执行 JS,仍可钓鱼/UI 伪造(<form>、<meta refresh>),且 CSP 一旦放宽即成可执行 XSS。修复:移除 dangerouslyUseHTMLString,error code 仅做日志
L-2 dc3/docker-compose.yml:31-46、docker-compose-db.yml:38-39,71-72、.env.example:24,36,82 硬编码默认口令 PostgreSQL/RabbitMQ/MQTT(EMQX) 默认 dc3/dc3dc3dc3。外部可达的中间件可用已知口令登录,读取全部点位/设备/用户数据。修复:compose 不留默认值强制注入
L-3 dc3-driver/dc3-driver-opc-ua/.../KeyLoader.java:63,74-80 硬编码密钥口令 PKCS12 keystore 默认口令 "password"(可 env 覆盖)。部署不改则客户端证书私钥可被提取。修复:未配置时启动报错
L-4 dc3-common-*/.../generator/MybatisGenerator.java(4 处) 硬编码 DB 口令 代码生成工具(独立 main)连库口令 dc3/dc3dc3dc3 写死。仅开发工具,但进入源码。修复:改环境变量读取
L-5 dc3-web/src/mock/handlers/auth.ts:21-23、src/views/login/Login.vue:139 硬编码登录凭据(前端 mock) mock 模式 DEFAULT_PASSWORD='dc3dc3dc3' 且登录页预填。仅 mock 构建生效;若误发布暴露默认凭据。修复:构建期注入
L-6 dc3-common/dc3-common-web/.../WebFluxSecurityConfig.java:172 + application-web.yml:50 未授权访问(Actuator) /actuator/** permitAll,暴露 health/prometheus/info/metrics。中心端口若被发布则未认证可读,泄露运行信息。修复:移除 permitAll 或限内网
L-7 dc3-web/src/components/link/ExternalLink.vue:19 Open Redirect / javascript: URI 通用外链组件 :href 未校验(死代码,无任何导入)。若未来接入需协议白名单或直接移除
L-8 dc3-driver/dc3-driver-can/.../CanDriverCustomServiceImpl.java:218 命令执行加固 唯一 OS 命令执行点 ProcessBuilder("sh","-c",...),当前有白名单 ^[A-Za-z0-9 _#,.=%/\-]+$ 拦截所有分隔符(已核实无法注入)。建议改为参数化 ProcessBuilder("cansend", iface, canId+"#"+data) 去掉 shell 面;另存在每读一次起 5s 子进程的低强度 DoS 面
L-9 dc3-common/dc3-common-rabbitmq/.../RabbitConfig.java:149 反序列化加固 JacksonJsonMessageConverter 内置 DefaultJackson2JavaTypeMapper 读消息头 __TypeId__。当前所有 @RabbitListener 入参为固定具体 DTO 且默认 TypePrecedence.INFERRED + 信任包检查,不可利用。建议显式 setTrustedPackages("io.github.pnoker.*") 收敛信任包,勿开宽泛信任

✅ 已核实为安全 / 误报排除

位置 结论
dc3-common-public/.../utils/X509Util.java:140 pemParser.readObject() 是 BouncyCastle PEM 解析,非 Java 反序列化;全项目无 ObjectInputStream/readUnshared
JsonUtil.java(唯一 ObjectMapper) 未开启 default typing,parseObject(..., Object.class) 仅产出 LinkedHashMap
PointCommandPayload.java:32-36(唯一 @JsonTypeInfo) sealed interface,@JsonSubTypes 仅 ReadPayload/WritePayload,无法指定任意类
DashboardMapper.xml(data)${bucket}/${dimension} 服务层 GRANULARITY 集合 + DIMENSION_COLUMN 映射表白名单后才拼入
DashboardMapper.xml(manager)${table} 调用处传硬编码字面量 "dc3_driver" 等
各 .apply()/.last() 调用 均为字面量("1 = 0"、LIMIT_ONE),last("LIMIT "+int) 参数为 int 无法注入
PostgresRepositoryServiceImpl 的 {0} 绑定参数 MyBatis-Plus 绑定参数,非字符串拼接
RabbitMQ / MQTT / HTTP / gRPC / TCP-UDP 外部数据 全部解析为固定具体类型或 Map/JsonNode/JsonNode 提取,无类型混淆
RenderedAssistantMessage.vue v-html LLM 输出先经 marked 再经 DOMPurify 3.4.12 消毒(覆盖 script/事件属性/javascript:/mutation XSS)
前端其余渲染路径 设备/点位/告警/日志均走 {{ }} 插值(Vue 默认转义);无 innerHTML/eval/document.write/富文本编辑器
window.open(Layout.vue:403) 硬编码 https://doc.dc3.site,不可控
MQTT broker 地址 / Webhook URL / MCP backendBase 均来自部署静态配置,非用户可写
BLE 驱动 new URL(adapterName, deviceAddress) 是 BlueCove 蓝牙 URL,非网络 SSRF
SnakeYAML 2.6 仅 Spring Boot 传递依赖,代码无 new Yaml() 解析不可信输入
log4j 使用 Logback,无 log4j JNDI 面

🎯 优先修复建议

  1. (P0)部署层密钥/口令治理:移除 .env.example 与 compose 中 DC3_SECURITY_KEY/AUTH_HMAC_SECRET/POSTGRES_PASSWORD/RABBITMQ_PASSWORD/MQTT_PASSWORD 的默认值并强制注入;KeyUtil 增加弱密钥 fail-fast(H-1 / H-6 / L-2)。
  2. (P0)种子管理员:initdb 移除 dc3/dc3dc3dc3 固定口令,改为首启随机生成 + 强制改密(H-5)。
  3. (P0)SSRF 集中治理:统一在驱动元数据装载处校验出站 host/ip/port(拒绝私网/元数据网段 + DNS rebinding),属性写入侧按属性 code 白名单(H-3 / H-4 / M-4)。
  4. (P0)ORDER BY 注入:PageUtil.page() 对 OrderItem.column 白名单 + 开启 injectionDetection(H-2)。
  5. (P1)信任边界:OAuth 公共端点不信任客户端 X-Auth-Principal 头(H-7);HMAC 全环境强制开启(H-6);gRPC 加鉴权 + 不暴露(M-5)。
  6. (P1)越权:Principal/Token 接口绑定当前主体与租户(M-2 / M-3)。
  7. (P2)纵深:前端通知去掉 dangerouslyUseHTMLString(L-1);CAN 驱动改参数化 ProcessBuilder(L-8);MQTT 私钥移出仓库(M-1)。

审计方式:多代理并行分类排查(SQL注入/命令执行/反序列化/硬编码凭据/未授权越权/SSRF/XSS)+ 关键高危点人工调用链核实。报告基于静态代码审计,未做动态验证(PoC 需在测试环境复现)。

正文完
 0