System Detail Endpoints Read Records Across Tenants Without a Tenant Guard
Summary
In SpringBlade, the detail-read endpoints GET /user/detail, GET /role/detail, and GET /dept/detail of the system service load the requested record by primary key with no tenant-ownership validation. Any tenant administrator who knows a target record identifier can read user, role, and department records belonging to another tenant or to the platform itself. The user record is returned including the password hash, email, and phone of the target account.
Details
SpringBlade's tenant isolation for these tables relies on explicit TenantGuard checks in handlers, because the MyBatis-Plus tenant interceptor covers only a small configured table list; blade_user, blade_role, and blade_dept are not auto-filtered.
The three detail handlers are annotated with a role check that every tenant administrator satisfies (tenant provisioning creates an admin-alias role for each tenant), and then perform a by-ID lookup with no tenant predicate and no guard. In the verified run, the response field filter applied to the user view did not strip the password field for a tenant-administrator caller.
The same service enforces the boundary correctly on its guarded write path: a cross-tenant department write is rejected with 无权操作非本租户的部门. The detail reads simply lack the equivalent check.
PoC
Tested on SpringBlade v5.0.1 at commit 0c180b9d0d68542ab4a2938e23989db7078832ca (open-source Cloud edition), with the blade-system service running in its documented standalone configuration.
Test conditions:
- the attacker was the administrator of a separately provisioned tenant, authenticating with that tenant's own admin account;
- the targets were the platform tenant's (
000000) seeded records: user1123598821738675201(admin), role1123598816738675201, department1123598813738675201.
Controls: a plain user token was rejected by GET /tenant/list with 请求未授权, and the same tenant-administrator token was rejected by a cross-tenant department write with 无权操作非本租户的部门.
The attacker read the platform administrator's user record:
GET /user/detail?id=1123598821738675201 HTTP/1.1
Host: <TARGET>
blade-auth: bearer <TENANT_ADMIN_JWT>Observed response (decisive fields):
{ "code": 200, "success": true, "data": {
"id": "1123598821738675201", "tenantId": "000000", "account": "admin",
"password": "<password digest>", "email": "[email protected]", "phone": "22233322"
} }The same token received code 200 with full records from GET /role/detail?id=1123598816738675201 and GET /dept/detail?id=1123598813738675201, both belonging to tenant 000000.
Expected behavior: a by-ID read should verify that the requested record belongs to the caller's tenant (or that the caller is the platform administrator) before returning it.
Impact
Every tenant administrator who knows a target record identifier can read user, role, and department records from other tenants and from the platform tenant. The user records include password digests generated by the project's unsalted MD5-then-SHA-1 construction, email addresses, and phone numbers — the platform administrator's own account among them — enabling offline password guessing and cross-tenant account discovery. The identifiers used in this PoC are published in the repository's SQL initialization scripts; exploitation of non-seeded records requires obtaining or inferring a suitable target identifier.
The defect is a cross-tenant confidentiality break on the read side of the system service, while the same service enforces the boundary on its guarded write paths.
Suggested remediation
Apply the existing tenant guard (or an explicit tenant comparison against the caller's tenant) to every by-ID read in the system controllers, mirroring the protection already present on the write paths. Password digests should also be excluded from all API responses, for example with @JsonIgnore or a dedicated response DTO that does not expose the password field.
Source: chillzhuang/SpringBlade