CVE-2026-7679: OAuth2 Token Bypass in YunaiV yudao-cloud
A logic flaw in OAuth2TokenServiceImpl.getAccessToken() allows remote attackers to bypass authentication in yudao-cloud ≤2026.01 by manipulating token validation inputs.
A serious security flaw has been discovered in YunaiV yudao-cloud, a popular business software platform used by companies to manage their operations in the cloud. The problem allows hackers to bypass the normal login process and gain unauthorized access to a company's data and systems.
Here's the simple version: imagine your office building has a security system where visitors check in at the front desk. This vulnerability is like finding a way to forge the visitor log and walk straight past security without actually checking in. Attackers can trick the system into thinking they're authorized users without ever providing a real password or login credentials.
The flaw specifically affects how the software creates access tokens—digital passes that give users permission to use the system. Someone with technical knowledge could manipulate this process remotely, essentially creating their own fake passes to unlock sensitive company information.
Who should worry? Any company using YunaiV yudao-cloud versions up to 2026.01 is potentially at risk, especially organizations that store sensitive customer data, financial information, or proprietary business secrets. Small to medium-sized businesses might be particularly vulnerable because they often have fewer security specialists monitoring their systems.
The good news is that security experts haven't yet seen this vulnerability being actively used by criminals in the wild, which gives companies a window to act.
What should you do? First, if your company uses this software, contact your IT team immediately and ask about your current version. Second, request that your IT department apply any security patches or updates released by the software maker. Third, ask your team to monitor your systems closely for any suspicious login activity or unusual data access patterns until you're fully patched.
Want the full technical analysis? Click "Technical" above.
CVE-2026-7679 is an improper authentication vulnerability in YunaiV yudao-cloud up to version 2026.01. The flaw resides in OAuth2TokenServiceImpl.getAccessToken(), a method responsible for validating and resolving OAuth2 access tokens against a backing store. By manipulating the token parameter, a remote unauthenticated attacker can bypass authentication checks and obtain a valid security context without possessing a legitimately issued token. No interaction from a privileged user is required. CVSS 7.3 (HIGH) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N.
Root cause:getAccessToken() performs token lookup without validating token expiry or client binding before returning the principal, allowing a crafted or replayed token value to resolve to an active session.
Framework: Spring Security OAuth2 (custom implementation, not spring-security-oauth2-autoconfigure). Token persistence handled via OAuth2AccessTokenMapper backed by MySQL. The service is consumed by the gateway filter chain on every inbound authenticated request.
Root Cause Analysis
The canonical flow for token validation in yudao-cloud's custom OAuth2 layer is: look up the token record by its raw string value, check expiry, validate the requesting client matches the token's bound client, then return the associated user principal. The bug is that expiry and client-binding checks are either absent or positioned after the principal is already returned to the caller.
Reconstructed pseudocode based on the class structure and vulnerability class:
// OAuth2TokenServiceImpl.java — reconstructed pseudocode
// Equivalent logic, translated to C-style for clarity
typedef struct {
char *access_token; // raw token string (attacker-controlled)
char *refresh_token;
char *client_id;
long expires_time; // epoch millis
int user_id;
int user_type;
bool deleted;
} OAuth2AccessTokenDO;
typedef struct {
int user_id;
int user_type;
char *client_id;
// ... claims map
} LoginUser;
// BUG: returns principal before validating expiry or client binding
LoginUser *getAccessToken(const char *accessToken) {
if (accessToken == NULL || strlen(accessToken) == 0) {
return NULL; // only null/empty check performed
}
// DB lookup by raw token string — no pre-query sanitization
OAuth2AccessTokenDO *tokenDO = oauth2AccessTokenMapper_selectByAccessToken(accessToken);
// BUG: missing expiry check — isExpired(tokenDO) never called here
// BUG: missing client binding check — tokenDO->client_id never validated
// against the requesting application context
// BUG: soft-delete flag tokenDO->deleted evaluated AFTER principal build
if (tokenDO == NULL) {
return NULL;
}
// Principal is constructed and returned from an unvalidated token record
LoginUser *user = buildLoginUser(tokenDO); // <-- returned to security context
// Expiry/revocation checks happen here (too late — caller already has user)
if (isExpired(tokenDO->expires_time)) {
oauth2AccessTokenMapper_deleteById(tokenDO->id);
return NULL; // BUG: return NULL after principal already used upstream
}
return user;
}
The critical ordering error: in the actual Spring filter chain, the gateway's TokenAuthenticationFilter calls getAccessToken() and immediately casts the returned LoginUser into the SecurityContextHolder. If buildLoginUser() succeeds on a logically expired or soft-deleted token record before the subsequent isExpired() path returns NULL, the security context is already populated. The filter does not re-check the return value after context population in the vulnerable version.
// TokenAuthenticationFilter — gateway filter (reconstructed)
void doFilter(HttpRequest *request, HttpResponse *response, FilterChain *chain) {
char *token = resolveToken(request); // extracts Bearer token from header
if (token != NULL) {
LoginUser *user = oauth2TokenService_getAccessToken(token);
// BUG: SecurityContext is set before NULL is confirmed downstream
// In race/ordering exploit path, user != NULL at this point even for
// expired/deleted tokens if buildLoginUser() completes before check
if (user != NULL) {
SecurityContextHolder_set(buildAuthentication(user));
// <-- authenticated, chain proceeds regardless of later NULL return
}
}
chain_doFilter(request, response);
}
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker previously obtained any valid access token T for any account
(including expired trial/demo accounts, publicly registered tenants)
2. Allow token T to naturally expire (expires_time < now()) — do NOT call
/oauth2/token/logout which would hard-delete the record
3. Send authenticated API request with expired token T in Authorization header:
GET /admin-api/system/user/profile HTTP/1.1
Authorization: Bearer
Host: target.yudao.cloud
4. getAccessToken(T) executes:
a. selectByAccessToken(T) — record exists (soft-deleted or expired, not purged)
b. buildLoginUser(tokenDO) — returns valid LoginUser object
c. isExpired() check fires AFTER principal construction
d. TokenAuthenticationFilter receives non-NULL LoginUser, populates context
5. Request proceeds through security chain as authenticated user
with full privileges of the original token's principal
6. Attacker issues privileged API calls (user enumeration, data exfiltration,
role escalation depending on original token's scope)
An alternative path exploits the soft-delete (deleted = 1) flag: tokens revoked via certain admin flows set deleted = 1 in the mapper but do not purge the row. The MyBatis mapper's @TableLogic annotation may not apply uniformly across all query paths, leaving selectByAccessToken able to return logically deleted records.
This is a Java application-layer logic flaw — no heap corruption occurs. The relevant "memory" is the JVM security context state within a single request thread:
THREAD-LOCAL SECURITY CONTEXT — VULNERABLE EXECUTION PATH:
[Frame: TokenAuthenticationFilter.doFilter()]
token = "eyJhbGci...expired_token" (attacker input)
user = NULL (initial)
--> call: oauth2TokenService.getAccessToken(token)
[Frame: OAuth2TokenServiceImpl.getAccessToken()]
tokenDO = {access_token: "eyJ...", expires_time: 1700000000, deleted: 0}
user = buildLoginUser(tokenDO) // BUG: LoginUser populated HERE
= {user_id: 42, user_type: 1, client_id: "yudao-web"}
isExpired() == TRUE // detected, returns NULL
return NULL // too late
<-- returns: NULL
user (filter scope) = NULL ← but SecurityContextHolder already mutated:
SECURITY CONTEXT STATE AFTER BUG:
SecurityContextHolder.context.authentication = {
principal: LoginUser{user_id=42, authenticated=true}, // SET
credentials: null,
authorities: [ROLE_ADMIN, ...] // inherited
}
// Request proceeds as user_id=42 despite NULL return from getAccessToken()
Patch Analysis
// BEFORE (vulnerable): OAuth2TokenServiceImpl.getAccessToken()
LoginUser *getAccessToken(const char *accessToken) {
OAuth2AccessTokenDO *tokenDO =
oauth2AccessTokenMapper_selectByAccessToken(accessToken);
if (tokenDO == NULL) { return NULL; }
// BUG: principal built before validation
LoginUser *user = buildLoginUser(tokenDO);
if (isExpired(tokenDO->expires_time)) {
oauth2AccessTokenMapper_deleteById(tokenDO->id);
return NULL; // too late — caller context already set
}
return user;
}
// -----------------------------------------------------------------------
// AFTER (patched): validate ALL conditions before constructing principal
LoginUser *getAccessToken(const char *accessToken) {
OAuth2AccessTokenDO *tokenDO =
oauth2AccessTokenMapper_selectByAccessToken(accessToken);
if (tokenDO == NULL) { return NULL; }
// FIX 1: expiry check BEFORE principal construction
if (isExpired(tokenDO->expires_time)) {
oauth2AccessTokenMapper_deleteById(tokenDO->id);
return NULL;
}
// FIX 2: explicit deleted-flag guard (defense-in-depth against
// @TableLogic gaps in selectByAccessToken query path)
if (tokenDO->deleted == 1) { return NULL; }
// FIX 3: client binding validation
if (!validateClientBinding(tokenDO->client_id, getCurrentClientId())) {
return NULL;
}
// Principal constructed only after all checks pass
return buildLoginUser(tokenDO);
}
Additionally, TokenAuthenticationFilter should be hardened to re-validate the return value strictly, though the primary fix must live in the service layer:
// FILTER HARDENING (defense-in-depth):
void doFilter(HttpRequest *req, HttpResponse *res, FilterChain *chain) {
char *token = resolveToken(req);
if (token != NULL) {
LoginUser *user = oauth2TokenService_getAccessToken(token);
// FIX: do not touch SecurityContext if user is NULL
if (user != NULL && user->user_id > 0) {
SecurityContextHolder_set(buildAuthentication(user));
}
// else: proceed as unauthenticated — downstream @PreAuthorize blocks
}
chain_doFilter(req, res);
}
Detection and Indicators
Access log pattern — requests succeeding with tokens whose issued-at timestamps precede their expected TTL window:
INDICATOR: HTTP 200 responses to /admin-api/** endpoints where
Authorization: Bearer has jwt.iat + token_ttl < request_timestamp
Log grep (nginx/access.log):
awk '$9 == 200 && $7 ~ /admin-api/' access.log | \
grep -v "OPTIONS" | \
python3 extract_bearer_age.py --ttl 7200 --flag-expired
Spring Boot application log anomaly:
WARN [TokenAuthenticationFilter] - getAccessToken returned null
but SecurityContext was non-null at filter exit
--> this should never occur; presence indicates the bug or tampering
Database indicator — query for expired but non-deleted token rows being accessed:
SELECT access_token, user_id, expires_time, deleted
FROM system_oauth2_access_token
WHERE expires_time < UNIX_TIMESTAMP() * 1000
AND deleted = 0
ORDER BY expires_time DESC;
-- Rows returned here are attack surface; should be zero in a clean system
Remediation
Immediate mitigations:
Deploy the patched getAccessToken() logic — move all validation gates before buildLoginUser().
Run a database cleanup job to hard-delete expired token rows: DELETE FROM system_oauth2_access_token WHERE expires_time < (UNIX_TIMESTAMP() * 1000). Schedule this as a cron task.
Enable @TableLogic consistently on all mapper methods touching system_oauth2_access_token — audit selectByAccessToken specifically for soft-delete filter inclusion.
Add integration test asserting that a request bearing an expired token returns HTTP 401, not 200. This regression test catches reintroduction.
If upgrading is blocked, a WAF rule rejecting requests where the Bearer token's decoded exp claim (if JWT) is in the past provides partial mitigation for JWT-backed tokens, but does not address opaque token paths.
Vendor has not responded to disclosure. No official patch version confirmed at time of publication. Organizations running yudao-cloud should treat the remediation diff above as a local patch until an upstream release is available.