Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
9d1cafce
authored
Apr 20, 2026
by
Jon
Committed by
GitHub
Apr 20, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(sandbox): fix unauthenticated RCE via code-server (#6781)
parent
6867c6e6
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
242 additions
and
8 deletions
+242
-8
packages/service/core/agentSkills/sandboxConfig.ts
+1
-1
projects/agent-sandbox/entrypoint.sh
+0
-4
projects/app/server.ts
+83
-1
projects/app/src/pageComponents/dashboard/skill/detail/config/SandboxIframe.tsx
+1
-1
projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts
+22
-0
projects/app/src/service/core/sandbox/proxy.ts
+28
-0
projects/app/src/service/core/sandbox/proxyUtils.ts
+107
-1
No files found.
packages/service/core/agentSkills/sandboxConfig.ts
View file @
9d1cafce
...
...
@@ -114,7 +114,7 @@ export function getSandboxDefaults(): SandboxDefaults {
},
workDirectory
:
'/home/sandbox/workspace'
,
// workDirectory: env.AGENT_SANDBOX_OPENSANDBOX_WORK_DIRECTORY ?? '/home/sandbox/workspace',
targetPort
:
8080
,
targetPort
:
44772
,
entrypoint
:
'/home/sandbox/entrypoint.sh'
// entrypoint: env.AGENT_SANDBOX_OPENSANDBOX_ENTRYPOINT ?? '/home/sandbox/entrypoint.sh'
};
...
...
projects/agent-sandbox/entrypoint.sh
View file @
9d1cafce
...
...
@@ -10,11 +10,7 @@ unset FASTGPT_SESSION_ID FASTGPT_WORKDIR FASTGPT_ENABLE_CODE_SERVER
# Start code-server or sleep forever
if
[
"
${
_ENABLE_CODE_SERVER
}
"
=
"true"
]
;
then
# --bind-addr 0.0.0.0:8080 allows access from outside the container
# --auth none removes password protection
exec
code-server
\
--bind-addr
0.0.0.0:8080
\
--auth
none
\
--disable-telemetry
\
--disable-update-check
\
--disable-workspace-trust
\
...
...
projects/app/server.ts
View file @
9d1cafce
...
...
@@ -34,10 +34,65 @@ async function main() {
// Import pure utilities from sandboxProxyUtils — no service-layer deps, safe in tsx CJS mode.
// getSandboxProxyTarget is NOT imported here; auth is delegated to the proxyAuth API route.
const
{
parseSubdomainProxy
,
rewriteHtml
,
redeemRelayToken
}
=
(
await
import
(
const
{
parseSubdomainProxy
,
rewriteHtml
,
redeemRelayToken
,
ensureCodeServerSession
,
deleteCsSession
}
=
(
await
import
(
'./src/service/core/sandbox/proxyUtils'
))
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
<
string
|
null
>
{
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
})
});
if
(
!
resp
.
ok
)
return
null
;
const
{
password
}
=
await
resp
.
json
();
return
password
||
null
;
}
catch
{
return
null
;
}
}
// Inject code-server session cookie into an outgoing request header object.
function
injectCsKey
(
reqHeaders
:
IncomingMessage
[
'headers'
],
key
:
string
):
void
{
const
existing
=
(
reqHeaders
.
cookie
as
string
|
undefined
)
??
''
;
const
stripped
=
existing
.
split
(
';'
)
.
map
((
s
)
=>
s
.
trim
())
.
filter
((
s
)
=>
!
s
.
toLowerCase
().
startsWith
(
'code-server-session='
))
.
join
(
'; '
);
reqHeaders
.
cookie
=
stripped
?
`
${
stripped
}
; code-server-session=
${
key
}
`
:
`code-server-session=
${
key
}
`
;
}
// Build the correct code-server login base URL.
// After prefix stripping, req.url starts with /proxy/8080/... when going through execd.
// In that case the login endpoint is at target/proxy/8080, not target/login directly.
function
deriveCsLoginTarget
(
target
:
string
,
url
:
string
):
string
{
const
m
=
url
.
match
(
/^
\/
proxy
\/(\d
+
)
/
);
return
m
?
`
${
target
}
/proxy/
${
m
[
1
]}
`
:
target
;
}
// Ensure code-server is authenticated and inject the session cookie into reqHeaders.
async
function
injectCodeServerAuth
(
reqHeaders
:
IncomingMessage
[
'headers'
],
sandboxId
:
string
,
target
:
string
):
Promise
<
void
>
{
const
key
=
await
ensureCodeServerSession
(
sandboxId
,
target
,
()
=>
fetchCodeServerPassword
(
sandboxId
)
);
if
(
key
)
injectCsKey
(
reqHeaders
,
key
);
}
const
proxy
=
httpProxy
.
createProxyServer
({
xfwd
:
true
,
changeOrigin
:
true
});
proxy
.
on
(
'error'
,
...
...
@@ -49,6 +104,22 @@ async function main() {
}
);
// Detect code-server session expiry: if the upstream returns a 302 to /login,
// evict the cached CS session so the next request triggers a fresh login.
proxy
.
on
(
'proxyRes'
,
(
proxyRes
,
req
)
=>
{
if
(
proxyRes
.
statusCode
===
302
&&
typeof
proxyRes
.
headers
.
location
===
'string'
&&
proxyRes
.
headers
.
location
.
includes
(
'/login'
)
)
{
const
sid
=
(
req
as
IncomingMessage
).
headers
[
'x-fastgpt-sandbox-id'
]
as
string
|
undefined
;
if
(
sid
)
{
dev
&&
console
.
log
(
`[proxy:cs] session expired, evicting csSession sandboxId=
${
sid
}
`
);
deleteCsSession
(
sid
);
}
}
});
// absproxy: fetch upstream then rewrite HTML paths with base prefix
async
function
handleAbsProxy
(
req
:
IncomingMessage
,
...
...
@@ -103,11 +174,16 @@ async function main() {
)
{
try
{
const
target
=
await
authProxyTarget
(
req
.
headers
,
sandboxId
,
portNum
);
const
csTarget
=
deriveCsLoginTarget
(
target
,
req
.
url
||
''
);
if
(
proxyType
===
'absproxy'
)
{
await
injectCodeServerAuth
(
req
.
headers
,
sandboxId
,
csTarget
);
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
);
// 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
,
{
target
,
headers
:
{
origin
:
`
${
targetUrl
.
protocol
}
//
${
targetUrl
.
host
}
`
}
...
...
@@ -157,6 +233,9 @@ async function main() {
try
{
const
target
=
await
authProxyTarget
(
req
.
headers
,
sandboxId
,
portNum
);
const
targetUrl
=
new
URL
(
target
);
const
csTarget
=
deriveCsLoginTarget
(
target
,
req
.
url
||
''
);
await
injectCodeServerAuth
(
req
.
headers
,
sandboxId
,
csTarget
);
req
.
headers
[
'x-fastgpt-sandbox-id'
]
=
sandboxId
;
proxy
.
web
(
req
,
res
,
{
target
,
headers
:
{
origin
:
`
${
targetUrl
.
protocol
}
//
${
targetUrl
.
host
}
`
}
...
...
@@ -352,6 +431,9 @@ async function main() {
// 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
);
req
.
headers
[
'x-fastgpt-sandbox-id'
]
=
sandboxId
;
proxy
.
ws
(
req
,
socket
,
head
,
{
target
,
headers
:
{
origin
:
`
${
targetUrl
.
protocol
}
//
${
targetUrl
.
host
}
`
}
...
...
projects/app/src/pageComponents/dashboard/skill/detail/config/SandboxIframe.tsx
View file @
9d1cafce
...
...
@@ -11,7 +11,7 @@ const SandboxIframe = () => {
return
(
<
Box
w=
{
'100%'
}
h=
{
'100%'
}
>
<
iframe
src=
{
sandboxEndpointUrl
}
src=
{
`${sandboxEndpointUrl}proxy/8080/`
}
sandbox=
"allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-same-origin"
referrerPolicy=
"no-referrer"
style=
{
{
...
...
projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts
0 → 100644
View file @
9d1cafce
import
type
{
NextApiRequest
,
NextApiResponse
}
from
'next'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
getCodeServerPasswordFromSandbox
}
from
'@/service/core/sandbox/proxy'
;
// Internal-only endpoint: read the code-server password from the container config.yaml.
// Called by server.ts (running in the same process) to avoid importing service packages directly.
// Only requests from 127.0.0.1 are accepted.
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
const
clientIp
=
req
.
socket
.
remoteAddress
;
if
(
clientIp
!==
'127.0.0.1'
&&
clientIp
!==
'::1'
&&
clientIp
!==
'::ffff:127.0.0.1'
)
{
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
password
=
await
getCodeServerPasswordFromSandbox
(
sandboxId
);
return
res
.
json
({
password
});
}
export
default
NextAPI
(
handler
);
projects/app/src/service/core/sandbox/proxy.ts
View file @
9d1cafce
...
...
@@ -3,6 +3,11 @@ import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import
{
parseHeaderCert
}
from
'@fastgpt/service/support/permission/auth/common'
;
import
type
{
IncomingHttpHeaders
}
from
'http'
;
import
{
upsertProxySession
,
getProxySession
}
from
'./proxyUtils'
;
import
{
getSandboxProviderConfig
,
connectToProviderSandbox
,
disconnectFromProviderSandbox
}
from
'@fastgpt/service/core/agentSkills/sandboxConfig'
;
const
dev
=
process
.
env
.
NODE_ENV
!==
'production'
;
...
...
@@ -59,3 +64,26 @@ export async function getSandboxProxyTarget(
upsertProxySession(sandboxId, authTeamId, host, protocol);
return `
$
{
protocol
}:
//${host}:${targetPort}`;
}
/**
* 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<string | null> {
const sandbox = await MongoSandboxInstance.findOne({ sandboxId }).lean();
if (!sandbox?.metadata?.providerSandboxId) return null;
const providerConfig = getSandboxProviderConfig();
const adapter = await connectToProviderSandbox(
providerConfig,
sandbox.metadata.providerSandboxId
);
try {
const result = await adapter.execute(
"grep '^password:' ~/.config/code-server/config.yaml 2>/dev/null | awk '{print $2}' | tr -d '[:space:]'"
);
return result.stdout.trim() || null;
} finally {
await disconnectFromProviderSandbox(adapter);
}
}
projects/app/src/service/core/sandbox/proxyUtils.ts
View file @
9d1cafce
...
...
@@ -116,6 +116,7 @@ export function getProxySession(sandboxId: string): ProxySession | null {
export
function
deleteProxySession
(
sandboxId
:
string
):
void
{
_sessionStore
().
delete
(
sandboxId
);
deleteCsSession
(
sandboxId
);
}
// Remove all proxy sessions belonging to a given team.
...
...
@@ -123,8 +124,113 @@ export function deleteProxySession(sandboxId: string): void {
export
function
deleteProxySessionsByTeam
(
teamId
:
string
):
void
{
const
store
=
_sessionStore
();
for
(
const
[
k
,
v
]
of
store
)
{
if
(
v
.
teamId
===
teamId
)
store
.
delete
(
k
);
if
(
v
.
teamId
===
teamId
)
{
store
.
delete
(
k
);
deleteCsSession
(
k
);
}
}
}
// ---- code-server session store ----
// Keyed by sandboxId; stores the `key` cookie value returned by code-server /login.
// TTL matches ProxySession so both expire around the same time.
const
CS_SESSION_TTL
=
2
*
60
*
60
*
1000
;
// 2 h
const
MAX_CS_SESSION_STORE_SIZE
=
1000
;
type
CsSession
=
{
keyCookie
:
string
;
exp
:
number
};
const
_csStore
=
():
Map
<
string
,
CsSession
>
=>
{
const
g
=
globalThis
as
any
;
if
(
!
g
.
__csSessionStore
)
g
.
__csSessionStore
=
new
Map
();
return
g
.
__csSessionStore
;
};
export
function
getCsSession
(
sandboxId
:
string
):
string
|
null
{
const
entry
=
_csStore
().
get
(
sandboxId
);
if
(
!
entry
||
entry
.
exp
<
Date
.
now
())
{
_csStore
().
delete
(
sandboxId
);
return
null
;
}
entry
.
exp
=
Date
.
now
()
+
CS_SESSION_TTL
;
return
entry
.
keyCookie
;
}
export
function
deleteCsSession
(
sandboxId
:
string
):
void
{
_csStore
().
delete
(
sandboxId
);
}
/**
* Ensure a valid code-server session exists for the given sandbox.
* Checks the in-process cache first; on miss invokes getPassword() and POSTs /login.
* Returns the `key` cookie value on success, or null on failure.
*/
export
async
function
ensureCodeServerSession
(
sandboxId
:
string
,
target
:
string
,
// e.g. "http://10.0.0.5:8080"
getPassword
:
()
=>
Promise
<
string
|
null
>
):
Promise
<
string
|
null
>
{
const
cached
=
getCsSession
(
sandboxId
);
if
(
cached
)
return
cached
;
const
password
=
await
getPassword
();
if
(
!
password
)
return
null
;
let
resp
:
Response
;
try
{
resp
=
await
fetch
(
`
${
target
}
/login`
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
,
origin
:
new
URL
(
target
).
origin
// scheme://host:port only, no path component
},
body
:
`password=
${
encodeURIComponent
(
password
)}
`
,
redirect
:
'manual'
// success → 302; wrong password → 200
});
}
catch
(
e
)
{
console
.
error
(
`[csLogin] fetch error sandboxId=
${
sandboxId
}
:
${(
e
as
Error
).
message
}
`
);
return
null
;
}
if
(
resp
.
status
!==
302
&&
resp
.
status
!==
301
)
{
console
.
warn
(
`[csLogin] unexpected status=
${
resp
.
status
}
sandboxId=
${
sandboxId
}
`
);
return
null
;
}
// Extract key=<value> from Set-Cookie header(s)
const
setCookies
:
string
[]
=
(
resp
.
headers
as
any
).
getSetCookie
?.()
??
[
resp
.
headers
.
get
(
'set-cookie'
)
??
''
];
let
keyCookie
:
string
|
null
=
null
;
for
(
const
h
of
setCookies
)
{
const
m
=
(
h
as
string
).
match
(
/
(?:
^|;
\s
*
)
code-server-session=
([^
;
]
+
)
/i
);
if
(
m
)
{
keyCookie
=
m
[
1
].
trim
();
break
;
}
}
if
(
!
keyCookie
)
{
console
.
warn
(
`[csLogin] no key cookie in response sandboxId=
${
sandboxId
}
`
);
return
null
;
}
const
csStore
=
_csStore
();
// Enforce capacity cap: evict the soonest-to-expire entry before adding a new one
if
(
!
csStore
.
has
(
sandboxId
)
&&
csStore
.
size
>=
MAX_CS_SESSION_STORE_SIZE
)
{
let
evictKey
:
string
|
null
=
null
;
let
minExp
=
Infinity
;
for
(
const
[
k
,
v
]
of
csStore
)
{
if
(
v
.
exp
<
minExp
)
{
minExp
=
v
.
exp
;
evictKey
=
k
;
}
}
if
(
evictKey
)
csStore
.
delete
(
evictKey
);
}
csStore
.
set
(
sandboxId
,
{
keyCookie
,
exp
:
Date
.
now
()
+
CS_SESSION_TTL
});
// Prune expired entries on each write
for
(
const
[
k
,
v
]
of
csStore
)
if
(
v
.
exp
<
Date
.
now
())
csStore
.
delete
(
k
);
return
keyCookie
;
}
// Rewrite absolute paths in HTML for the absproxy mode
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment