fix(chat): restrict generated-file link regex to http(s)/relative URLs

Follow-up to #447. The generated-file link extraction accepted any
non-')' text before the path, so a paren-free javascript:/data: URL
embedding /api/v1/files/generated/<id> could be captured and bound to an
<a href>, enabling XSS on click. Adopt the scheme-restricted pattern
already used by SegmentSupersedeDetector and the channel adapters, on
both backend (ChatController) and frontend (useChat). Also replace the
inline fully-qualified Pattern/Matcher with imports and drop an unused
run-overview i18n key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mateaix 2026-06-28 16:19:27 +08:00
parent 3c6c765e51
commit 83660893a6
4 changed files with 6 additions and 6 deletions

View File

@ -37,6 +37,8 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
@ -1733,8 +1735,8 @@ public class ChatController {
/** Markdown link pointing at a generated-file download URL. Used by the
* StreamAccumulator to surface generated artifacts in the run-overview rail. */
private static final java.util.regex.Pattern GENERATED_FILE_LINK_PATTERN =
java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\(([^)]*?/api/v1/files/generated/[a-zA-Z0-9-]+)\\)");
private static final Pattern GENERATED_FILE_LINK_PATTERN =
Pattern.compile("\\[([^\\]]+)\\]\\(((?:https?://[^/\\s)\\]]+)?/api/v1/files/generated/[A-Za-z0-9-]+)\\)");
/**
* 流式累积器 收集 StreamDelta 事件持久化到 DB
@ -2044,7 +2046,7 @@ public class ChatController {
* produce duplicate entries in the run-overview rail. */
private void extractGeneratedFiles(String result, String toolName) {
if (result == null || result.isBlank()) return;
java.util.regex.Matcher m = GENERATED_FILE_LINK_PATTERN.matcher(result);
Matcher m = GENERATED_FILE_LINK_PATTERN.matcher(result);
while (m.find()) {
String url = m.group(2);
boolean dup = generatedFiles.stream()

View File

@ -418,7 +418,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
}
/** Markdown link pointing at a generated-file download URL. */
const GENERATED_FILE_LINK_RE = /\[([^\]]+)\]\(([^)]*?\/api\/v1\/files\/generated\/[a-zA-Z0-9-]+)\)/g
const GENERATED_FILE_LINK_RE = /\[([^\]]+)\]\(((?:https?:\/\/[^/\s)\]]+)?\/api\/v1\/files\/generated\/[A-Za-z0-9-]+)\)/g
/** Extract generated-file artifacts from a tool result string. */
function extractGeneratedFiles(result: unknown, toolName: string): GeneratedFile[] {

View File

@ -106,7 +106,6 @@ export default {
plan: 'Plan Progress',
subagents: 'Sub-agents',
files: 'Generated Files',
noFiles: 'No generated files yet',
noPlan: 'No execution plan yet',
noSubagents: 'No sub-agents yet',
collapse: 'Collapse',

View File

@ -106,7 +106,6 @@ export default {
plan: '计划进度',
subagents: '子 Agent',
files: '生成文件',
noFiles: '暂无生成文件',
noPlan: '暂无执行计划',
noSubagents: '暂无子 Agent',
collapse: '收起总览',