{
  "ticket_id": "CVE-2026-23958",
  "code_root": "external/dataease",
  "source": {
    "type": "cve",
    "cve_id": "CVE-2026-23958",
    "advisory_url": "https://www.ox.security/blog/blog-dataease-cve-2026-23958-admin-takeover/",
    "chain_article": "https://www.ox.security/blog/from-auth-bypass-to-rce-a-4-vulnerability-exploit-chain-in-dataease/",
    "vendor": "FIT2CLOUD / DataEase",
    "product": "DataEase",
    "repo": "https://github.com/dataease/dataease"
  },
  "facts": {
    "cve_id": "CVE-2026-23958",
    "issue_summary": "DataEase signs and verifies its authentication JWTs with a key derived from the admin password. In the community edition (no commercial 'loginServer' bean) the secret is computed as MD5(SubstituleLoginConfig.getPwd()) inside io.dataease.auth.filter.CommunityTokenFilter, and the default install never rotates the password 'DataEase@123456'. An unauthenticated attacker who knows (or guesses) the password — and on any default deployment that password is the well-known constant 'DataEase@123456' — can forge a JWT with arbitrary uid/oid claims signed with HMAC-SHA256(MD5(pwd)), satisfy both TokenFilter (which calls JWT.decode without verifying the signature) and CommunityTokenFilter (which calls algorithm.verify / verifier.verify against the password-derived secret), and authenticate as any user, including the admin (uid=1, oid=1).",
    "vulnerability_type": "Authentication bypass via predictable JWT signing key (password-derived HMAC secret, default credential)",
    "suspected_cwe": ["CWE-916", "CWE-287", "CWE-321", "CWE-798"],
    "affected_versions": "DataEase <= v2.10.20 (per the OX article, fix shipped in v2.10.19 / v2.10.21 release cycle)",
    "fixed_versions": ["v2.10.21"],
    "reproduce_version": "v2.10.20",
    "verify_fixed_version": "v2.10.21",
    "repo_url": "https://github.com/dataease/dataease.git",
    "checkout_ref": "v2.10.20",
    "code_root": "external/dataease",
    "attacker_access": "anonymous / unauthenticated (only requires network reach to the DataEase API; default-credential deployments are trivially exploitable, otherwise the MD5-of-password secret is brute-forceable offline against any captured/forged token)",
    "primary_entry_point": "Any authenticated DataEase REST endpoint reachable through TokenFilter + CommunityTokenFilter. Recommended target: GET /de2api/user/personInfo (returns the authenticated user record — confirms admin takeover with a single anonymous HTTP request and a forged Bearer token).",
    "vulnerable_files": [
      "sdk/common/src/main/java/io/dataease/auth/filter/CommunityTokenFilter.java",
      "sdk/common/src/main/java/io/dataease/auth/config/SubstituleLoginConfig.java",
      "sdk/common/src/main/java/io/dataease/auth/filter/TokenFilter.java"
    ],
    "patch_commit": "cac165e (fix: JWT Token 漏洞, 2025-12-25 by fit2cloud-chenyw)",
    "patch_diff_command": "git -C external/dataease diff v2.10.20 v2.10.21 -- sdk/common/src/main/java/io/dataease/auth/filter/CommunityTokenFilter.java",
    "root_cause_summary": "CommunityTokenFilter derives the JWT HMAC secret from the admin password (MD5 of the substitute-login password in the community edition, or the raw getPwd() return value via reflection on the apisix user cache in the X-Pack edition). The password is a deterministic, low-entropy, attacker-knowable value — defaulting to the documented constant 'DataEase@123456' — so an attacker can sign their own JWTs. The v2.10.21 patch (commit cac165e) changes the reflective lookup from getPwd() to getSecret() on the user cache object so the secret is no longer derived from the user password.",
    "reproduction_requirement": "MUST exploit a running DataEase v2.10.20 server over real HTTP. The attacker is anonymous: no login, no token capture, no client of any kind. The attacker forges a JWT signed with HMAC-SHA256(MD5('DataEase@123456')) carrying claims {uid: 1, oid: 1} and presents it as a Bearer token (or via the X-DE-TOKEN / DE-XPACK-AUTH-TOKEN header DataEase uses) to a protected API endpoint, then receives the admin user record in a 200 response. The identical request against v2.10.21 must return 401. Hand-rolled unit tests that call the filter classes directly are NOT acceptable — the server must be live.",
    "exploit_outline": [
      "1. Start DataEase v2.10.20 via the official docker compose (Java/Spring Boot server + MySQL + Redis). Wait for the API to be ready on http://127.0.0.1:8100 (or whichever port the compose maps).",
      "2. Sanity-check anonymously: curl -i http://127.0.0.1:8100/de2api/user/personInfo -> 401.",
      "3. Forge a JWT with header {alg: HS256, typ: JWT}, payload {uid: 1, oid: 1}, signed with HMAC-SHA256 over the standard 'header.payload' using the secret = MD5('DataEase@123456') (lowercase hex). A few lines of Python with PyJWT or a manual base64url+HMAC implementation is sufficient.",
      "4. Send the forged token as the X-DE-TOKEN header (DataEase's auth header) to a protected endpoint, e.g. curl -i -H \"X-DE-TOKEN: <forged>\" http://127.0.0.1:8100/de2api/user/personInfo -> 200 with admin user JSON.",
      "5. Tear down v2.10.20, bring up v2.10.21, repeat step 4 with the exact same forged token -> 401 (signature now uses getSecret(), no longer derivable from the public default password)."
    ],
    "vulnerable_indicator": "On v2.10.20 the forged Bearer/X-DE-TOKEN JWT yields HTTP 200 with the admin user record from /de2api/user/personInfo (or any other authenticated endpoint).",
    "fixed_indicator": "On v2.10.21 the identical forged token yields HTTP 401 with the DE-GATEWAY-FLAG header set, because verifier.verify() now fails (secret is no longer derivable from the admin password).",
    "environment_notes": "Use the docker backend. DataEase v2.x publishes a runnable docker compose stack (Spring Boot app + MySQL 8 + Redis). The agent can either (a) pull the official 'dataease/dataease' image at the chosen tag, or (b) clone the repo at the chosen tag and run the bundled docker-compose. The forging script needs only Python's stdlib hashlib + hmac + base64. No external network calls beyond image/code fetches.",
    "default_credentials": {
      "username": "admin",
      "password": "DataEase@123456",
      "source": "SubstituleLoginConfig.java: dataease.default-pwd = 'DataEase@123456'"
    },
    "desired_artifacts": [
      "repro/reproduction_steps.sh",
      "repro/rca_report.md",
      "repro/patch_analysis.md",
      "captured HTTP transcripts (200 on v2.10.20, 401 on v2.10.21) plus the forged JWT and the MD5(password) derivation"
    ]
  },
  "simulation": {
    "recipe_id": "default_recipe",
    "inputs": {
      "code_root": "external/dataease",
      "install_command": "git clone --depth 1 --branch v2.10.20 https://github.com/dataease/dataease.git external/dataease",
      "command": "git -C external/dataease diff v2.10.20 v2.10.21 -- sdk/common/src/main/java/io/dataease/auth/filter/CommunityTokenFilter.java"
    }
  }
}
