From d379eda74a8f5087488df958419ba2989b67692e Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Sat, 11 Jul 2026 22:43:27 +0800 Subject: [PATCH] fix(frontend): harden frontend server (RCE/SSRF/path traversal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close four issues a security scan flagged on a publicly-exposed `veadk frontend` deployment: - RCE / arbitrary write: remove the "test run" feature — /web/deploy-temp-agent (POST/DELETE), the temp-agent registry and the AgentLoader monkeypatch. It wrote client-supplied files to disk and exec'd agent.py, i.e. unauthenticated remote code execution. - SSRF: /agentkit-proxy now requires X-AgentKit-Key and only proxies to https://*.volceapi.com (host checked via urlparse, so userinfo tricks like https://x.volceapi.com@evil.com are rejected). - Arbitrary file read: the SPA fallback confines FileResponse to the built UI directory, blocking ../ traversal (e.g. /../../etc/passwd). - Arbitrary write (path traversal): /web/deploy-agentkit rejects absolute or ".." file paths that resolve outside its temp dir. Also drops the now-unused AgentTest UI + client helpers and rebuilds the shipped web assets. --- frontend/src/adk/client.ts | 28 -- frontend/src/ui/AgentTest.css | 237 ----------- frontend/src/ui/AgentTest.tsx | 243 ------------ frontend/src/ui/ProjectPreview.tsx | 41 +- veadk/cli/cli_frontend.py | 189 ++------- veadk/webui/assets/index-8o8Ic942.css | 10 - .../{index-CELjG15D.js => index-BoPazl5W.js} | 375 +++++++++--------- veadk/webui/assets/index-CyCROncT.css | 10 + veadk/webui/index.html | 4 +- 9 files changed, 244 insertions(+), 893 deletions(-) delete mode 100644 frontend/src/ui/AgentTest.css delete mode 100644 frontend/src/ui/AgentTest.tsx delete mode 100644 veadk/webui/assets/index-8o8Ic942.css rename veadk/webui/assets/{index-CELjG15D.js => index-BoPazl5W.js} (54%) create mode 100644 veadk/webui/assets/index-CyCROncT.css diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index bc4e3b2b..5834063a 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -287,34 +287,6 @@ export async function* runSSE({ } } -/** Deploy a temporary agent for testing. */ -export async function deployTempAgent( - name: string, - files: { path: string; content: string }[], -): Promise { - const res = await apiFetch("/web/deploy-temp-agent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, files }), - }); - if (!res.ok) { - const err = await res.text(); - throw new Error(`Deploy failed: ${err}`); - } - const data = await res.json(); - return data.appName; -} - -/** Delete a temporary agent. */ -export async function deleteTempAgent(appName: string): Promise { - const res = await apiFetch(`/web/deploy-temp-agent/${appName}`, { - method: "DELETE", - }); - if (!res.ok && res.status !== 404) { - throw new Error(`Delete failed: ${res.status}`); - } -} - export interface DeployAgentkitResult { apikey: string; url: string; diff --git a/frontend/src/ui/AgentTest.css b/frontend/src/ui/AgentTest.css deleted file mode 100644 index a02c4dca..00000000 --- a/frontend/src/ui/AgentTest.css +++ /dev/null @@ -1,237 +0,0 @@ -.agent-test { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - flex: 1; - background: #ffffff; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06); - border: 1px solid #e8e8e8; -} - -.agent-test-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 24px; - border-bottom: 1px solid #e8e8e8; - background: #fafafa; -} - -.agent-test-title { - font-size: 15px; - font-weight: 500; - color: #262626; - margin: 0; - letter-spacing: -0.01em; -} - -.agent-test-close { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: none; - background: transparent; - color: #8c8c8c; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; -} - -.agent-test-close:hover { - background: #f0f0f0; - color: #262626; -} - -.agent-test-loading, -.agent-test-error { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - gap: 12px; - padding: 48px 24px; - color: var(--text-muted); -} - -.agent-test-error { - flex-direction: row; - color: var(--red); -} - -.agent-test-error .icon { - flex-shrink: 0; - width: 20px; - height: 20px; -} - -.error-title { - font-weight: 600; - margin-bottom: 4px; -} - -.error-message { - font-size: 14px; - color: var(--text-muted); -} - -.agent-test-messages { - flex: 1; - overflow-y: auto; - padding: 24px; - display: flex; - flex-direction: column; - gap: 20px; - background: #ffffff; -} - -.agent-test-empty { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: #bfbfbf; - font-size: 14px; -} - -.agent-test-message { - display: flex; - flex-direction: column; - gap: 6px; - max-width: 70%; -} - -.agent-test-message.user { - align-self: flex-end; -} - -.agent-test-message.assistant { - align-self: flex-start; -} - -.message-role { - font-size: 12px; - font-weight: 500; - color: #8c8c8c; - padding: 0 4px; -} - -.message-content { - background: #f5f5f5; - border-radius: 16px; - padding: 12px 16px; - color: #262626; - font-size: 14px; - line-height: 1.6; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-word; - max-width: 100%; - min-height: 20px; - display: block; -} - -.agent-test-message.user .message-content { - background: #0066ff !important; - color: white !important; - box-shadow: 0 1px 4px rgba(0, 102, 255, 0.2); -} - -.message-text { - display: block; - min-height: 1em; -} - - -.message-error { - display: flex; - align-items: flex-start; - gap: 8px; - color: var(--red); -} - -.message-error .icon { - flex-shrink: 0; - width: 16px; - height: 16px; - margin-top: 2px; -} - -.agent-test-input { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 24px; - border-top: 1px solid #e8e8e8; - background: #fafafa; -} - -.agent-test-input .input { - flex: 1; - padding: 10px 16px; - border: 1px solid #d9d9d9; - border-radius: 8px; - background: #ffffff; - color: #262626; - font-size: 14px; - outline: none; - transition: all 0.2s; -} - -.agent-test-input .input:focus { - border-color: #0066ff; - box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1); -} - -.agent-test-input .input:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.send-button { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border: none; - border-radius: 8px; - background: #0066ff; - color: white; - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 2px 4px rgba(0, 102, 255, 0.2); -} - -.send-button:hover:not(:disabled) { - background: #0052cc; - box-shadow: 0 4px 8px rgba(0, 102, 255, 0.3); -} - -.send-button:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; -} - -.send-button .icon { - width: 18px; - height: 18px; -} - -.icon.spin { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/frontend/src/ui/AgentTest.tsx b/frontend/src/ui/AgentTest.tsx deleted file mode 100644 index 19346519..00000000 --- a/frontend/src/ui/AgentTest.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { Loader2, Send, X, AlertCircle } from "lucide-react"; -import { - createSession, - deleteTempAgent, - deployTempAgent, - runSSE, -} from "../adk/client"; -import type { ProjectFile } from "../create/project"; -import { applyEvent, emptyAcc, type Block } from "../blocks"; -import { Blocks } from "./Blocks"; -import "./AgentTest.css"; - -interface AgentTestProps { - projectName: string; - files: ProjectFile[]; - onClose: () => void; -} - -interface Message { - role: "user" | "assistant"; - content: string; - blocks?: Block[]; - error?: string; -} - -export function AgentTest({ projectName, files, onClose }: AgentTestProps) { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const [deploying, setDeploying] = useState(true); - const [deployError, setDeployError] = useState(null); - const [appName, setAppName] = useState(null); - const [sessionId, setSessionId] = useState(null); - const messagesEndRef = useRef(null); - - // Auto-scroll to bottom when messages change - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); - - // Deploy agent on mount - useEffect(() => { - async function deploy() { - try { - setDeploying(true); - setDeployError(null); - - // Deploy the agent - const name = await deployTempAgent(projectName, files); - setAppName(name); - - // Create a session - const sid = await createSession(name, "test_user"); - setSessionId(sid); - - setDeploying(false); - } catch (err) { - console.error("Deploy failed:", err); - setDeployError(err instanceof Error ? err.message : String(err)); - setDeploying(false); - } - } - - deploy(); - - // Cleanup on unmount - return () => { - if (appName) { - deleteTempAgent(appName).catch((err) => - console.error("Failed to cleanup temp agent:", err) - ); - } - }; - }, [projectName, files]); - - async function handleSend() { - if (!input.trim() || loading || !appName || !sessionId) return; - - const userMessage: Message = { role: "user", content: input }; - const userInput = input; - setInput(""); - setLoading(true); - - const assistantMessage: Message = { - role: "assistant", - content: "", - blocks: [], - }; - - // Add both user and assistant messages at once to avoid state update race - setMessages((prev) => [...prev, userMessage, assistantMessage]); - - try { - let acc = emptyAcc(); - let fullText = ""; - - for await (const event of runSSE({ - appName, - userId: "test_user", - sessionId, - text: userInput, - })) { - // Check for errors - const error = event.error || event.errorMessage || event.error_message; - if (error) { - assistantMessage.error = String(error); - break; - } - - // Apply event to accumulator - acc = applyEvent(acc, event); - - // Extract text content from blocks - fullText = acc.blocks - .filter((b) => b.kind === "text") - .map((b) => (b as { text: string }).text) - .join(""); - - // Update message with streaming content - setMessages((prev) => { - const updated = [...prev]; - const lastMsg = updated[updated.length - 1]; - if (lastMsg && lastMsg.role === "assistant") { - lastMsg.content = fullText; - lastMsg.blocks = acc.blocks; - } - return updated; - }); - } - } catch (err) { - console.error("Run failed:", err); - assistantMessage.error = err instanceof Error ? err.message : String(err); - setMessages((prev) => { - const updated = [...prev]; - const lastMsg = updated[updated.length - 1]; - if (lastMsg && lastMsg.role === "assistant") { - lastMsg.error = assistantMessage.error; - } - return updated; - }); - } finally { - setLoading(false); - } - } - - return ( -
-
-

测试运行: {projectName}

- -
- - {deploying ? ( -
- - 正在部署 Agent... -
- ) : deployError ? ( -
- -
-
部署失败
-
{deployError}
-
-
- ) : ( - <> -
- {messages.length === 0 && ( -
- 输入消息开始测试你的 Agent -
- )} - {messages.map((msg, i) => ( -
-
- {msg.role === "user" ? "你" : projectName} -
-
- {msg.role === "user" ? ( -
{msg.content}
- ) : msg.error ? ( -
- - {msg.error} -
- ) : msg.blocks && msg.blocks.length > 0 ? ( - {}} /> - ) : ( -
{msg.content || "..."}
- )} -
-
- ))} -
-
- -
- setInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }} - disabled={loading} - /> - -
- - )} -
- ); -} diff --git a/frontend/src/ui/ProjectPreview.tsx b/frontend/src/ui/ProjectPreview.tsx index 9d56823d..223d78ff 100644 --- a/frontend/src/ui/ProjectPreview.tsx +++ b/frontend/src/ui/ProjectPreview.tsx @@ -7,7 +7,6 @@ import { Folder, Loader2, Pencil, - Play, Rocket, Trash2, } from "lucide-react"; @@ -36,7 +35,6 @@ hljs.registerLanguage("dockerfile", dockerfile); hljs.registerLanguage("makefile", makefile); import type { AgentProject, ProjectFile } from "../create/project"; import { buildZip } from "./zip"; -import { AgentTest } from "./AgentTest"; import "./ProjectPreview.css"; // --- syntax highlighting ---------------------------------------------------- @@ -187,7 +185,6 @@ export function ProjectPreview({ project, onChange, onDeploy, onAgentAdded }: Pr const [deployError, setDeployError] = useState(null); const [deployResult, setDeployResult] = useState(null); const [addingAgent, setAddingAgent] = useState(false); - const [testing, setTesting] = useState(false); const underlayRef = useRef(null); const tree = useMemo(() => { @@ -455,27 +452,15 @@ export function ProjectPreview({ project, onChange, onDeploy, onAgentAdded }: Pr )} {project.files.length > 0 && ( - <> - - - + )} {onDeploy && (