mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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;
|
||||
|
||||
String visitorId = request.getVisitorId() != null ? request.getVisitorId() : UUID.randomUUID().toString();
|
||||
|
||||
// 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),
|
||||
// never accepted as a raw conversationId — so a caller can't reach another tenant's history.
|
||||
final String visitorId;
|
||||
final String effectiveSessionId;
|
||||
try {
|
||||
visitorId = normalizeVisitorId(request.getVisitorId());
|
||||
effectiveSessionId = normalizeSessionId(request.getSessionId());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
sendErrorAndComplete(emitter, ex.getMessage());
|
||||
@ -156,7 +156,7 @@ public class WebChatController {
|
||||
// 创建或获取会话(workspace 从 agent 获取)
|
||||
var webAgent = agentService.getAgent(resolvedAgentId);
|
||||
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());
|
||||
@ -296,8 +296,10 @@ public class WebChatController {
|
||||
}
|
||||
String base = deriveConversationId(apiKey, visitorId, null);
|
||||
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
|
||||
&& owner.equals(c.getUsername())
|
||||
&& (c.getConversationId().equals(base) || c.getConversationId().startsWith(prefix)))
|
||||
.map(c -> {
|
||||
String cid = c.getConversationId();
|
||||
@ -391,13 +393,61 @@ public class WebChatController {
|
||||
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。
|
||||
* <p>conversation_id 列为 VARCHAR(64);当 visitorId + sessionId 过长导致超出列宽时,
|
||||
* 把可变部分折叠为稳定哈希,保证 id 唯一且有界(否则 INSERT 会在 /stream 处 500)。
|
||||
*/
|
||||
private String deriveConversationId(String apiKey, String visitorId, String sessionId) {
|
||||
String base = "webchat:" + apiKey.substring(0, Math.min(8, apiKey.length())) + ":" + visitorId;
|
||||
return sessionId != null ? base + ":" + sessionId : base;
|
||||
static String deriveConversationId(String apiKey, String visitorId, String sessionId) {
|
||||
String key8 = apiKey.substring(0, Math.min(8, apiKey.length()));
|
||||
String full = "webchat:" + key8 + ":" + visitorId + (sessionId != null ? ":" + sessionId : "");
|
||||
if (full.length() <= 64) {
|
||||
return full;
|
||||
}
|
||||
return "webchat:" + key8 + ":#"
|
||||
+ sha256Hex(visitorId + " | ||||