From c0a922432011f946feb2faf99232b45e98f48951 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 23 Apr 2026 15:08:34 +0800 Subject: [PATCH] fix: run agent-sandbox container as non-root user --- .../service/core/agentSkills/sandboxConfig.ts | 2 +- projects/agent-sandbox/Dockerfile | 15 ++++---- projects/agent-sandbox/entrypoint.sh | 2 +- projects/app/server.ts | 34 +++++++++++-------- .../src/pages/api/core/sandbox/proxyAuth.ts | 8 +++-- .../pages/api/core/sandbox/proxyCSPassword.ts | 6 ++-- .../app/src/service/core/sandbox/proxy.ts | 18 +++++++--- 7 files changed, 52 insertions(+), 33 deletions(-) diff --git a/packages/service/core/agentSkills/sandboxConfig.ts b/packages/service/core/agentSkills/sandboxConfig.ts index 7c2e59fe7611..91532f171928 100644 --- a/packages/service/core/agentSkills/sandboxConfig.ts +++ b/packages/service/core/agentSkills/sandboxConfig.ts @@ -115,7 +115,7 @@ export function getSandboxDefaults(): SandboxDefaults { workDirectory: '/home/sandbox/workspace', // workDirectory: env.AGENT_SANDBOX_OPENSANDBOX_WORK_DIRECTORY ?? '/home/sandbox/workspace', targetPort: 44772, - entrypoint: '/home/sandbox/entrypoint.sh' + entrypoint: '/opt/entrypoint.sh' // entrypoint: env.AGENT_SANDBOX_OPENSANDBOX_ENTRYPOINT ?? '/home/sandbox/entrypoint.sh' }; } diff --git a/projects/agent-sandbox/Dockerfile b/projects/agent-sandbox/Dockerfile index 6ef10636c1a9..178a2b4a92e3 100644 --- a/projects/agent-sandbox/Dockerfile +++ b/projects/agent-sandbox/Dockerfile @@ -26,21 +26,22 @@ RUN rm -rf /var/lib/apt/lists/* # Install code-server using the official script RUN curl -fsSL https://code-server.dev/install.sh | sh +# Copy and configure entrypoint +COPY entrypoint.sh /opt/entrypoint.sh +RUN chmod +x /opt/entrypoint.sh + # Create a non-root user for security RUN useradd --create-home --shell /bin/bash sandbox -USER root +USER sandbox + WORKDIR /home/sandbox # Copy VS Code settings RUN mkdir -p /home/sandbox/.local/share/code-server/User -COPY --chown=sandbox:sandbox settings.json /home/sandbox/.local/share/code-server/User/settings.json - -# Copy and configure entrypoint -COPY --chown=sandbox:sandbox entrypoint.sh /home/sandbox/entrypoint.sh -RUN chmod +x /home/sandbox/entrypoint.sh +COPY settings.json /home/sandbox/.local/share/code-server/User/settings.json # Expose code-server port EXPOSE 8080 -ENTRYPOINT ["/home/sandbox/entrypoint.sh"] +ENTRYPOINT ["/opt/entrypoint.sh"] diff --git a/projects/agent-sandbox/entrypoint.sh b/projects/agent-sandbox/entrypoint.sh index 0c2edb1bcb43..1dd3c3a1e839 100644 --- a/projects/agent-sandbox/entrypoint.sh +++ b/projects/agent-sandbox/entrypoint.sh @@ -16,7 +16,7 @@ if [ "${_ENABLE_CODE_SERVER}" = "true" ]; then --disable-workspace-trust \ --disable-getting-started-override \ --app-name "Skills" \ - --user-data-dir /home/sandbox/.local/share/code-server \ + --user-data-dir ~/.local/share/code-server \ "${WORKDIR}" else exec sleep infinity diff --git a/projects/app/server.ts b/projects/app/server.ts index d21c8ab88561..02ddb7c4f20f 100644 --- a/projects/app/server.ts +++ b/projects/app/server.ts @@ -45,12 +45,15 @@ async function main() { )) as typeof import('./src/service/core/sandbox/proxyUtils'); // Fetch the code-server password from the container config.yaml via the internal API. - async function fetchCodeServerPassword(sandboxId: string): Promise { + async function fetchCodeServerPassword( + sandboxId: string, + teamId: string + ): Promise { try { const resp = await fetch(`http://127.0.0.1:${port}/api/core/sandbox/proxyCSPassword`, { method: 'POST', headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ sandboxId }) + body: JSON.stringify({ sandboxId, teamId }) }); if (!resp.ok) return null; const { password } = await resp.json(); @@ -85,10 +88,11 @@ async function main() { async function injectCodeServerAuth( reqHeaders: IncomingMessage['headers'], sandboxId: string, - target: string + target: string, + teamId: string ): Promise { const key = await ensureCodeServerSession(sandboxId, target, () => - fetchCodeServerPassword(sandboxId) + fetchCodeServerPassword(sandboxId, teamId) ); if (key) injectCsKey(reqHeaders, key); } @@ -173,15 +177,15 @@ async function main() { proxyType: string ) { try { - const target = await authProxyTarget(req.headers, sandboxId, portNum); + const { target, teamId } = await authProxyTarget(req.headers, sandboxId, portNum); const csTarget = deriveCsLoginTarget(target, req.url || ''); if (proxyType === 'absproxy') { - await injectCodeServerAuth(req.headers, sandboxId, csTarget); + await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId); await handleAbsProxy(req, res, target, sandboxId, String(portNum)); } else { // Rewrite Origin so code-server's CSRF check passes (changeOrigin only rewrites Host). const targetUrl = new URL(target); - await injectCodeServerAuth(req.headers, sandboxId, csTarget); + await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId); // Mark the request so the proxyRes handler can identify the sandbox on session expiry. req.headers['x-fastgpt-sandbox-id'] = sandboxId; proxy.web(req, res, { @@ -231,10 +235,10 @@ async function main() { } try { - const target = await authProxyTarget(req.headers, sandboxId, portNum); + const { target, teamId } = await authProxyTarget(req.headers, sandboxId, portNum); const targetUrl = new URL(target); const csTarget = deriveCsLoginTarget(target, req.url || ''); - await injectCodeServerAuth(req.headers, sandboxId, csTarget); + await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId); req.headers['x-fastgpt-sandbox-id'] = sandboxId; proxy.web(req, res, { target, @@ -304,7 +308,7 @@ async function main() { let target: string; try { - target = await authProxyTarget(req.headers, tunnelSandboxId, tunnelPort); + ({ target } = await authProxyTarget(req.headers, tunnelSandboxId, tunnelPort)); dev && console.log(`[proxy:tcptunnel] auth ok target=${target}`); } catch (err: any) { const status = err.statusCode || 502; @@ -426,13 +430,13 @@ async function main() { ); try { - const target = await authProxyTarget(req.headers, sandboxId, portNum); + const { target, teamId } = await authProxyTarget(req.headers, sandboxId, portNum); dev && console.log(`[proxy:ws] auth ok, forwarding to target=${target}`); // Rewrite Origin to match the target host so code-server's CSRF check passes. // changeOrigin:true only rewrites Host, not Origin. const targetUrl = new URL(target); const csTarget = deriveCsLoginTarget(target, req.url || ''); - await injectCodeServerAuth(req.headers, sandboxId, csTarget); + await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId); req.headers['x-fastgpt-sandbox-id'] = sandboxId; proxy.ws(req, socket, head, { target, @@ -457,7 +461,7 @@ async function authProxyTarget( reqHeaders: IncomingMessage['headers'], sandboxId: string, targetPort: number -): Promise { +): Promise<{ target: string; teamId: string }> { dev && console.log( `[proxy:auth] POST proxyAuth sandboxId=${sandboxId} port=${targetPort} hasCookie=${!!reqHeaders.cookie}` @@ -483,9 +487,9 @@ async function authProxyTarget( throw Object.assign(new Error(msg), { statusCode: code }); } - const { target } = await authResp.json(); + const { target, teamId } = await authResp.json(); dev && console.log(`[proxy:auth] proxyAuth ok target=${target}`); - return target as string; + return { target: target as string, teamId: teamId as string }; } // Build upstream request headers, dropping hop-by-hop headers diff --git a/projects/app/src/pages/api/core/sandbox/proxyAuth.ts b/projects/app/src/pages/api/core/sandbox/proxyAuth.ts index 02b79ec2e059..c31ddc3cfabc 100644 --- a/projects/app/src/pages/api/core/sandbox/proxyAuth.ts +++ b/projects/app/src/pages/api/core/sandbox/proxyAuth.ts @@ -20,8 +20,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return Promise.reject('Missing sandboxId or targetPort'); } - const target = await getSandboxProxyTarget(req.headers, sandboxId, Number(targetPort)); - return res.json({ target }); + const { target, teamId } = await getSandboxProxyTarget( + req.headers, + sandboxId, + Number(targetPort) + ); + return res.json({ target, teamId }); } // GET handler: redirect flow for subdomain cookie hand-off. diff --git a/projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts b/projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts index 06e5b950d5aa..db7985583de9 100644 --- a/projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts +++ b/projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts @@ -12,10 +12,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return res.status(403).json({ error: 'Internal only' }); } - const { sandboxId } = req.body as { sandboxId?: string }; - if (!sandboxId) return res.status(400).json({ error: 'Missing sandboxId' }); + const { sandboxId, teamId } = req.body as { sandboxId?: string; teamId?: string }; + if (!sandboxId || !teamId) return res.status(400).json({ error: 'Missing sandboxId or teamId' }); - const password = await getCodeServerPasswordFromSandbox(sandboxId); + const password = await getCodeServerPasswordFromSandbox(sandboxId, teamId); return res.json({ password }); } diff --git a/projects/app/src/service/core/sandbox/proxy.ts b/projects/app/src/service/core/sandbox/proxy.ts index 1687e74023d0..0013d0682f85 100644 --- a/projects/app/src/service/core/sandbox/proxy.ts +++ b/projects/app/src/service/core/sandbox/proxy.ts @@ -17,7 +17,7 @@ export async function getSandboxProxyTarget( headers: IncomingHttpHeaders, sandboxId: string, targetPort: number -): Promise { +): Promise<{ target: string; teamId: string }> { if (targetPort < 1 || targetPort > 65535) { throw Object.assign(new Error('Invalid port'), { statusCode: 400 }); } @@ -39,7 +39,10 @@ export async function getSandboxProxyTarget( const session = getProxySession(sandboxId); if (session) { dev && console.log(`[sandboxProxy] session fallback sandboxId=${sandboxId}`); - return `${session.protocol}://${session.host}:${targetPort}`; + return { + target: `${session.protocol}://${session.host}:${targetPort}`, + teamId: session.teamId + }; } throw Object.assign(new Error('Unauthorized'), { statusCode: 401 }); } @@ -62,17 +65,24 @@ export async function getSandboxProxyTarget( const { host, protocol } = sandbox.metadata!.endpoint!; // Cache target for subsequent cookie-less sub-requests (sandboxed iframe) upsertProxySession(sandboxId, authTeamId, host, protocol); - return `${protocol}://${host}:${targetPort}`; + return { target: `${protocol}://${host}:${targetPort}`, teamId: authTeamId }; } /** * Read the code-server password from the container's config.yaml via exec. * Returns null if the sandbox is not found, has no providerSandboxId, or exec fails. */ -export async function getCodeServerPasswordFromSandbox(sandboxId: string): Promise { +export async function getCodeServerPasswordFromSandbox( + sandboxId: string, + teamId: string +): Promise { const sandbox = await MongoSandboxInstance.findOne({ sandboxId }).lean(); if (!sandbox?.metadata?.providerSandboxId) return null; + if (String(sandbox.metadata?.teamId) !== teamId) { + throw Object.assign(new Error('Access denied'), { statusCode: 403 }); + } + const providerConfig = getSandboxProviderConfig(); const adapter = await connectToProviderSandbox( providerConfig,