| Title | macrozheng mall 1.0.3 CWE-613: Insufficient Session Expiration |
|---|
| Description | macrozheng mall contains an improper account deactivation handling vulnerability in its JWT-based authentication mechanism.
When an administrator disables an account, the application only checks the account's enabled status during the login process. Already-issued JWT tokens are not invalidated, and subsequent authentication performed by JwtAuthenticationTokenFilter does not verify whether the corresponding account is still enabled.
As a result, a user who obtained a valid JWT before account deactivation can continue accessing authenticated administrative endpoints until the token expires.
Furthermore, the /admin/refreshToken endpoint can be used with the still-valid token to obtain a newly generated token. The refresh implementation verifies only the JWT signature and expiration status and does not check whether the associated account has been disabled. This allows the authenticated session to be continuously extended and makes the account deactivation mechanism ineffective.
Root Cause
1. Account status is checked only during login
UmsAdminServiceImpl.login() explicitly checks whether the account is enabled before issuing a JWT:
UserDetails userDetails = loadUserByUsername(username);
if(!passwordEncoder.matches(password,userDetails.getPassword())){
Asserts.fail("密码不正确");
}
if(!userDetails.isEnabled()){
Asserts.fail("帐号已被禁用");
}
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
Therefore, a disabled account cannot obtain a new token through the normal login process.
However, this check is not performed when an existing JWT is subsequently used.
2. JWT authentication does not verify the current account status
JwtAuthenticationTokenFilter.doFilterInternal() extracts the username from the supplied JWT and loads the current UserDetails:
String authToken = authHeader.substring(this.tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
if (username != null &&
SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails =
this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
The authentication decision is delegated to JwtTokenUtil.validateToken().
However, validateToken() only verifies that the username in the token matches the loaded user and that the token has not expired:
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username != null
&& username.equals(userDetails.getUsername())
&& !isTokenExpired(token);
}
It does not check:
userDetails.isEnabled()
Therefore, even when the database account status has changed from enabled to disabled, a previously issued and unexpired JWT is still accepted and a new authenticated SecurityContext is created.
The AdminUserDetails implementation correctly maps the account status to Spring Security's isEnabled() method:
@Override
public boolean isEnabled() {
return umsAdmin.getStatus().equals(1);
}
However, this property is checked during login but is not enforced by the JWT authentication filter.
3. The refresh-token mechanism does not check account status
The administrative controller exposes:
@RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
@ResponseBody
public CommonResult refreshToken(HttpServletRequest request) {
String token = request.getHeader(tokenHeader);
String refreshToken = adminService.refreshToken(token);
if (refreshToken == null) {
return CommonResult.failed("token已经过期!");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", refreshToken);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
The service simply forwards the supplied token to:
@Override
public String refreshToken(String oldToken) {
return jwtTokenUtil.refreshHeadToken(oldToken);
}
JwtTokenUtil.refreshHeadToken() verifies the JWT signature and checks whether the token has expired:
Map<String, Object> payload = getPayloadFromToken(token);
if (payload == null) {
return null;
}
if (isTokenExpired(token)) {
return null;
}
It does not load the associated account and does not verify its current status.
If the original JWT remains valid, the application generates another JWT with a new expiration time:
payload.put(CLAIM_KEY_CREATED, new Date());
return generateToken(payload);
Consequently, disabling the account does not invalidate the existing authentication chain.
Proof of Concept
The vulnerability was reproduced using a dedicated test account.
Step 1 — Login before account deactivation
Authenticate as a valid administrative user:
POST /admin/login
Content-Type: application/json
{
"username": "test1",
"password": "<valid-password>"
}
The application returns a valid JWT:
Authorization: Bearer <JWT>
The token is initially valid and can be used to access protected administrative endpoints.
Step 2 — Verify that the token provides authenticated access
Use the previously issued token to access a protected endpoint:
GET /admin/list
Authorization: Bearer <JWT>
The request is successfully authenticated.
Step 3 — Disable the account
An administrator changes the test1 account status to disabled through the account management functionality.
The account is now disabled in the database.
The application also prevents the disabled account from performing a new normal login because UmsAdminServiceImpl.login() checks userDetails.isEnabled().
Step 4 — Reuse the previously issued JWT
Without obtaining a new login token, send the original JWT again:
GET /admin/list
Authorization: Bearer <JWT>
The request remains authenticated and the protected endpoint is accessible.
This demonstrates that changing the account status to disabled does not revoke already-issued JWT credentials.
Step 5 — Refresh the still-valid JWT
While the original JWT remains unexpired, request:
GET /admin/refreshToken
Authorization: Bearer <JWT>
The application returns a newly generated JWT.
The refresh operation succeeds because refreshHeadToken() validates only the token itself and its expiration status; it does not verify the current account status.
Step 6 — Continue using the refreshed token
The newly generated token can then be used to access protected administrative endpoints:
GET /admin/list
Authorization: Bearer <NEW_JWT>
The account remains disabled, but the newly refreshed JWT is accepted.
This allows the authentication session to be continuously extended while the underlying account remains disabled.
Impact
An attacker who obtains a valid JWT before an administrator disables the associated account can continue using the credential after deactivation.
Without the refresh mechanism, the attacker can retain authenticated access until the original JWT expires.
More importantly, /admin/refreshToken allows an unexpired token belonging to the disabled account to be refreshed without checking the account's current status. This allows the attacker to continuously extend the authentication lifetime and effectively bypass the administrator's account deactivation action.
Depending on the privileges assigned to the affected account, this can result in continued unauthorized access to administrative functionality, including operations that modify application data, manage users, or change system configuration.
The vulnerability therefore defeats the security expectation that disabling an account immediately prevents that account from performing further authenticated operations. |
|---|
| Source | ⚠️ https://github.com/macrozheng/mall/issues/997 |
|---|
| User | mjh_123 (UID 92618) |
|---|
| Submission | 09/04/2026 04:53 (1 month ago) |
|---|
| Moderation | 10/11/2026 10:29 (1 month later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 416564 [macrozheng mall up to 1.0.3 JwtAuthenticationTokenFilter /admin/refreshToken JwtTokenUtil.refreshHeadToken token session expiration] |
|---|
| Points | 20 |
|---|