mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
fix(channel): bound webchat conversationId/username to prevent VARCHAR(64) overflow
The webchat conversationId (webchat:<key8>:<visitorId>[:<sessionId>]) and the derived username (webchat:<visitorId>) are written to VARCHAR(64) columns, but visitorId had no validation and sessionId allows 64 chars — so a long visitorId, or a legitimate 64-char sessionId, overflows the column and the getOrCreateConversation INSERT throws (500 on /stream). Validate visitorId (charset + blank->UUID) and fold the variable part into a stable hash when the derived id/username would exceed 64 chars, keeping short ids byte-identical (backward compatible). Also make listSessions filter on exact owner username, not just the conversationId prefix, so system-owned rows can never leak via a crafted visitorId. Adds boundary regression tests.
This commit is contained in:
parent
77b6baeccc
commit
846c1c31ca
@ -115,13 +115,13 @@ public class WebChatController {
|
|||||||
}
|
}
|
||||||
final Long resolvedAgentId = agentId;
|
final Long resolvedAgentId = agentId;
|
||||||
|
|
||||||
String visitorId = request.getVisitorId() != null ? request.getVisitorId() : UUID.randomUUID().toString();
|
|
||||||
|
|
||||||
// Optional sessionId lets one visitor hold multiple isolated threads. It is only ever
|
// Optional sessionId lets one visitor hold multiple isolated threads. It is only ever
|
||||||
// composed into the server-derived conversationId (kept under the key+visitor namespace),
|
// composed into the server-derived conversationId (kept under the key+visitor namespace),
|
||||||
// never accepted as a raw conversationId — so a caller can't reach another tenant's history.
|
// never accepted as a raw conversationId — so a caller can't reach another tenant's history.
|
||||||
|
final String visitorId;
|
||||||
final String effectiveSessionId;
|
final String effectiveSessionId;
|
||||||
try {
|
try {
|
||||||
|
visitorId = normalizeVisitorId(request.getVisitorId());
|
||||||
effectiveSessionId = normalizeSessionId(request.getSessionId());
|
effectiveSessionId = normalizeSessionId(request.getSessionId());
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
sendErrorAndComplete(emitter, ex.getMessage());
|
sendErrorAndComplete(emitter, ex.getMessage());
|
||||||
@ -156,7 +156,7 @@ public class WebChatController {
|
|||||||
// 创建或获取会话(workspace 从 agent 获取)
|
// 创建或获取会话(workspace 从 agent 获取)
|
||||||
var webAgent = agentService.getAgent(resolvedAgentId);
|
var webAgent = agentService.getAgent(resolvedAgentId);
|
||||||
Long webWsId = webAgent != null ? webAgent.getWorkspaceId() : 1L;
|
Long webWsId = webAgent != null ? webAgent.getWorkspaceId() : 1L;
|
||||||
var conv = conversationService.getOrCreateConversation(conversationId, resolvedAgentId, "webchat:" + visitorId, webWsId);
|
var conv = conversationService.getOrCreateConversation(conversationId, resolvedAgentId, webchatUsername(visitorId), webWsId);
|
||||||
|
|
||||||
// 保存用户消息
|
// 保存用户消息
|
||||||
conversationService.saveMessage(conversationId, "user", message, List.of());
|
conversationService.saveMessage(conversationId, "user", message, List.of());
|
||||||
@ -296,8 +296,10 @@ public class WebChatController {
|
|||||||
}
|
}
|
||||||
String base = deriveConversationId(apiKey, visitorId, null);
|
String base = deriveConversationId(apiKey, visitorId, null);
|
||||||
String prefix = base + ":";
|
String prefix = base + ":";
|
||||||
List<WebChatSessionView> sessions = conversationService.listConversations("webchat:" + visitorId).stream()
|
String owner = webchatUsername(visitorId);
|
||||||
|
List<WebChatSessionView> sessions = conversationService.listConversations(owner).stream()
|
||||||
.filter(c -> c.getConversationId() != null
|
.filter(c -> c.getConversationId() != null
|
||||||
|
&& owner.equals(c.getUsername())
|
||||||
&& (c.getConversationId().equals(base) || c.getConversationId().startsWith(prefix)))
|
&& (c.getConversationId().equals(base) || c.getConversationId().startsWith(prefix)))
|
||||||
.map(c -> {
|
.map(c -> {
|
||||||
String cid = c.getConversationId();
|
String cid = c.getConversationId();
|
||||||
@ -391,13 +393,61 @@ public class WebChatController {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Pattern VISITOR_ID_PATTERN = Pattern.compile("[A-Za-z0-9_.:\\-]{1,128}");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化调用方传入的 visitorId:空白 → 新 UUID;非空必须满足白名单字符集,否则抛出。
|
||||||
|
* 限制字符集既防注入/控制字符,也为派生的 conversationId / username 提供可预期的边界。
|
||||||
|
*/
|
||||||
|
private String normalizeVisitorId(String raw) {
|
||||||
|
if (raw == null || raw.trim().isEmpty()) {
|
||||||
|
return UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
String s = raw.trim();
|
||||||
|
if (!VISITOR_ID_PATTERN.matcher(s).matches()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid visitorId (allowed: letters, digits, '-', '_', '.', ':', length 1-128)");
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 由服务端拼装 conversationId,始终钳在 key + visitor 命名空间内。
|
* 由服务端拼装 conversationId,始终钳在 key + visitor 命名空间内。
|
||||||
* 绝不接受调用方传入的裸 conversationId。
|
* 绝不接受调用方传入的裸 conversationId。
|
||||||
|
* <p>conversation_id 列为 VARCHAR(64);当 visitorId + sessionId 过长导致超出列宽时,
|
||||||
|
* 把可变部分折叠为稳定哈希,保证 id 唯一且有界(否则 INSERT 会在 /stream 处 500)。
|
||||||
*/
|
*/
|
||||||
private String deriveConversationId(String apiKey, String visitorId, String sessionId) {
|
static String deriveConversationId(String apiKey, String visitorId, String sessionId) {
|
||||||
String base = "webchat:" + apiKey.substring(0, Math.min(8, apiKey.length())) + ":" + visitorId;
|
String key8 = apiKey.substring(0, Math.min(8, apiKey.length()));
|
||||||
return sessionId != null ? base + ":" + sessionId : base;
|
String full = "webchat:" + key8 + ":" + visitorId + (sessionId != null ? ":" + sessionId : "");
|
||||||
|
if (full.length() <= 64) {
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
return "webchat:" + key8 + ":#"
|
||||||
|
+ sha256Hex(visitorId + " | ||||||