Evidence-backed draft · source analysis only. Code excerpts come from the pinned Wukong_HRM commit and read-only PoCs cited below. No Wukong_HRM server or third-party target was contacted by this application.
Authentication is implemented as an aspect
Wukong_HRM uses Spring AOP to run authentication logic around mapped controller methods. Instead of every endpoint calling an authentication helper directly, ParamAspect.around() reads the request token, resolves a user from Redis, places that user in UserUtil, and then allows the controller to proceed.
That architecture makes the aspect a high-value trust boundary. If it fails open, many unrelated controllers inherit the same bypass.
Read the diagram as text
An HTTP request is mapped to a controller method. ParamAspect intercepts that method, reads the authentication token, checks Redis, and stores a user object in UserUtil before allowing controller and service logic to run.
The critical question is what happens when the header is absent or the cache lookup fails. Secure middleware should stop at that point. In the affected code, a default object makes the intended null check unreachable.
The bug is visible in four lines
At commit 186115e1…, ParamAspect initializes info with a new object before it validates the token:
String token = request.getHeader(Const.DEFAULT_TOKEN_NAME);
UserInfo info = new UserInfo();
if (StrUtil.isNotEmpty(token)
&& redis.exists(token + Const.TOKEN_CACHE_NAME)) {
info = redis.get(token);
}
if (ObjectUtil.isNull(info)) {
throw new NoLoginException();
}
UserUtil.setUser(info);
Consider the tokenless path one statement at a time:
request.getHeader(...)returnsnullor an empty value.new UserInfo()creates a non-null object.StrUtil.isNotEmpty(token)is false, so Redis is not consulted.ObjectUtil.isNull(info)is false becauseinfostill references that new object.- The aspect stores an empty
UserInfoand calls the controller.
The exception exists, but the default initialization prevents the missing-token path from reaching it. This is a fail-open state initialization bug, not a cryptographic weakness in the token itself.
Read the diagram as text
An unauthenticated client sends a request without a token. ParamAspect creates an empty but non-null UserInfo. It skips Redis, the null check evaluates false, and the aspect stores the empty identity before proceeding to the controller.
Why the pointcut increases the blast radius
The @Around expression targets request-mapped controller methods across com.kakarote.*.controller namespaces:
@Around(
"(execution(* com.kakarote.*.controller..*(..)) || " +
" execution(* com.kakarote.*.*.controller..*(..))) && " +
"execution(@(org.springframework.web.bind.annotation.*Mapping) * *(..)) && " +
"!execution(@(com.kakarote.core.common.ParamAspect) * *(..))"
)
public Object around(ProceedingJoinPoint point) throws Throwable {
// authentication context setup
return point.proceed();
}
The precise endpoint impact still depends on other checks in each controller, service, gateway, or employee-scoping aspect. But the central gate no longer guarantees that a request has an authenticated principal. Endpoints that assume ParamAspect already performed that work can become reachable with an empty context.
This is why the CVE is described as an authentication bypass across HRM APIs rather than one isolated missing annotation.
A concrete read-only path: attachment download
The public PoC repository documents a bounded, read-only example around:
GET /adminFile/download/{fileId} HTTP/1.1
Host: wukong-hrm.local
# Deliberately no AUTH-TOKEN header
Its decision logic does not treat any HTTP 200 as proof. It requires a non-empty body and records details such as the content type and disposition:
resp = client.get(DOWNLOAD % file_id, anonymous=True)
record = {
"http_status": resp.status,
"bytes": len(resp.body or b""),
"content_disposition": resp.content_disposition,
"gated": resp.verdict_class in ("NOT_LOGIN", "NO_AUTH"),
}
confirmed = (
record["http_status"] == 200
and record["bytes"] > 0
)
That distinction matters. A redirect, gateway-generated page, empty body, or nonexistent file is not the same as retrieving a stored attachment. A defensible reproduction needs a synthetic file created specifically for the lab and must compare anonymous and authenticated requests.
Read the diagram as text
Create one synthetic private attachment and record its identifier. Request only that identifier anonymously, validate status and known content, compare an authenticated control, apply the authentication fix, and repeat both requests.
The PoC also describes an optional fileList/{batchId} oracle, but a minimal lab should avoid broad enumeration. If the test already knows the synthetic file ID, no discovery step is needed.
A second example shows why authentication and authorization are separate
Another cited PoC targets POST /hrmEmployee/export with a body containing {"pageType": 0}. Its source comments trace the route through controller, service, and mapper code and assert that pageType=0 can raise the page limit to 10,000 rows.
A safe request shape for an isolated fixture is:
POST /hrmEmployee/export HTTP/1.1
Host: wukong-hrm.local
Content-Type: application/json
# no AUTH-TOKEN in the anonymous case
{"pageType": 0, "deptId": "synthetic-lab-department"}
The response assertion should check for a workbook signature only after populating the lab with synthetic employees:
XLSX_MAGIC = b"PK\x03\x04"
confirmed = (
response.status == 200
and response.body.startswith(XLSX_MAGIC)
and b"LAB-EMPLOYEE" in parsed_synthetic_rows
)
This route illustrates two layers:
- The
ParamAspectflaw can remove the authentication prerequisite. - A route that exports too broad a population can have a separate authorization/data-scope issue even for a logged-in user.
Fixing the first does not automatically prove the second is fixed. A technical test plan should keep separate assertions for anonymous, ordinary-employee, and HR-administrator sessions.
Evidence limitations and a responsible local reproduction
A complete lab needs more than running the PoC against an arbitrary URL. It should pin commit 186115e1…, start only required Wukong_HRM services on a private VM network, and populate synthetic records.
Recommended test matrix:
| Case | Token | Fixture | Expected vulnerable result | Expected fixed result |
|---|---|---|---|---|
| Attachment control | valid HR token | known lab file | known file returned | known file returned |
| Attachment bypass | absent | same known file | known file returned if affected | 401/403 before controller |
| Export control | valid HR token | two synthetic employees | workbook containing fixtures | workbook containing fixtures |
| Export bypass | absent | same fixtures | workbook if affected | 401/403 before export logic |
| Invalid ID | absent | nonexistent file | must not count as proof | must not count as proof |
Capture the HTTP status, relevant response headers, a hash of the synthetic response body, application logs showing which controller ran, and the exact commit. Do not retain real employee or attachment data.
This service has not completed that environment build, so the article remains source analysis only rather than “reproduced locally.” The existing QEMU preflight established repository evidence but not application behavior.
What a fail-closed fix should look like
The narrow initialization correction is to start with no identity and require a successful lookup:
String token = request.getHeader(Const.DEFAULT_TOKEN_NAME);
UserInfo info = null;
if (StrUtil.isNotEmpty(token)
&& redis.exists(token + Const.TOKEN_CACHE_NAME)) {
info = redis.get(token);
}
if (ObjectUtil.isNull(info)) {
throw new NoLoginException();
}
UserUtil.setUser(info);
That snippet expresses the intended invariant, but production remediation should be reviewed more broadly:
- A missing, expired, malformed, or unknown token should all fail before
point.proceed(). - Authentication should preferably live in a well-tested Spring Security filter/interceptor rather than depend only on mutable thread-local context.
- Controller methods handling attachments, exports, payroll, or employee records need object- and role-level authorization after authentication.
UserUtil.removeUser()should remain infinallyso pooled request threads do not retain a previous identity.- Regression tests should assert both status and the absence of controller/service side effects.
Detection and mitigation
Follow the upstream/CNA guidance for a fixed revision when one is published and verify that deployed services actually include it. Until then, restrict HRM APIs to trusted networks or an authenticated gateway, but treat that as defense in depth.
Review access logs for sensitive routes with no AUTH-TOKEN, especially successful attachment downloads, employee exports, payslip reads, and bulk list operations. A gateway may rewrite or strip headers, so correlate gateway and application logs before drawing conclusions.
Sources
- https://github.com/WuKongOpenSource/Wukong_HRM/blob/186115e1a5a0b827ad9596ff8c2f3a876fb0cc55/common/common-web/src/main/java/com/kakarote/core/config/ParamAspect.java#L35-L55
- https://github.com/AnkesKasty/cve-request-poc/blob/a7b6d1423555812d8efa26c91c5d0683164b31e5/Wukong_HRM/poc_attachment_download_idor.py
- https://github.com/AnkesKasty/cve-request-poc/blob/a7b6d1423555812d8efa26c91c5d0683164b31e5/Wukong_HRM/poc_employee_export_pii.py
- https://github.com/AnkesKasty/cve-request-poc/blob/a7b6d1423555812d8efa26c91c5d0683164b31e5/Wukong_HRM/poc_salary_slip_detail_read.py
- https://www.vulncheck.com/advisories/wukong-hrm-through-commit-186115e-authentication-bypass-via-paramaspect