Test sandbox (#4547)

* feat: python sandbox execute with a temporary file (#4464)

* change runPythonSandbox:
1. write code into a temp file in /tmp dir then run it
2. write sandbox python script into a tmp file then run it

* repair subProcess.py file does not generate in tmp dir

* Adjust the security policy to kill (#4546)

---------

Co-authored-by: Donald Yang <yjyangfan@gmail.com>
Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com>
This commit is contained in:
Archer 2025-04-15 16:26:10 +08:00 committed by GitHub
parent 97a6c6749a
commit 0c9e56c1ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 18 deletions

View File

@ -3,14 +3,16 @@
该目录为 FastGPT 主项目。 该目录为 FastGPT 主项目。
- app fastgpt 核心应用。 - app fastgpt 核心应用。
- sandbox 沙盒项目,用于运行工作流里的代码执行 需求python环境为python:3.11额外安装的包请于requirements.txt填写同时注意个别包可能额外安装库如pandas需要安装libffi - sandbox 沙盒项目,用于运行工作流里的代码执行 需求python环境为python:3.11额外安装的包请于requirements.txt填写在运行时会读取安装。
- 新加入python包遇见超时或者权限拦截的问题(确定不是自己的语法问题)请进入docker容器内部执行以下指令
- 注意个别安装的包可能需要额外安装库如pandas需要安装libffi
- 新加入python的包遇见超时或者权限拦截的问题(确定不是自己的语法问题)请进入docker容器内部执行以下指令
```shell ```shell
docker exec -it 《替换成容器名》 /bin/bash docker exec -it 《替换成容器名》 /bin/bash
chmod -x testSystemCall.sh chmod -x testSystemCall.sh
bash ./testSystemCall.sh bash ./testSystemCall.sh
``` ```
然后将新的数组替换src下sandbox的constants.py中的SYSTEM_CALLS数组即可
然后将新的数组替换或追加到src下sandbox的constants.py中的SYSTEM_CALLS数组即可

View File

@ -1,4 +1,5 @@
export const pythonScript = ` export const pythonScript = `
import os
import subprocess import subprocess
import json import json
import ast import ast
@ -20,6 +21,7 @@ def extract_imports(code):
seccomp_prefix = """ seccomp_prefix = """
from seccomp import * from seccomp import *
import sys import sys
import errno
allowed_syscalls = [ allowed_syscalls = [
"syscall.SYS_ARCH_PRCTL", "syscall.SYS_BRK", "syscall.SYS_CLONE", "syscall.SYS_ARCH_PRCTL", "syscall.SYS_BRK", "syscall.SYS_CLONE",
"syscall.SYS_CLOSE", "syscall.SYS_EPOLL_CREATE1", "syscall.SYS_EXECVE", "syscall.SYS_CLOSE", "syscall.SYS_EPOLL_CREATE1", "syscall.SYS_EXECVE",
@ -99,22 +101,22 @@ def run_pythonCode(data:dict):
variables = data["variables"] variables = data["variables"]
imports = "\\n".join(extract_imports(code)) imports = "\\n".join(extract_imports(code))
var_def = "" var_def = ""
output_code = "res = main(" output_code = "if __name__ == '__main__':\\n res = main("
for k, v in variables.items(): for k, v in variables.items():
if isinstance(v, str): one_var = f"{k} = {json.dumps(v)}\\n"
one_var = k + " = \\"" + v + "\\"\\n"
else:
one_var = k + " = " + str(v) + "\\n"
var_def = var_def + one_var var_def = var_def + one_var
output_code = output_code + k + ", " output_code = output_code + k + ", "
if output_code[-1] == "(": if output_code[-1] == "(":
output_code = output_code + ")\\n" output_code = output_code + ")\\n"
else: else:
output_code = output_code[:-2] + ")\\n" output_code = output_code[:-2] + ")\\n"
output_code = output_code + "print(res)" output_code = output_code + " print(res)"
code = imports + "\\n" + seccomp_prefix + "\\n" + var_def + "\\n" + code + "\\n" + output_code code = imports + "\\n" + seccomp_prefix + "\\n" + var_def + "\\n" + code + "\\n" + output_code
tmp_file = os.path.join(data["tempDir"], "subProcess.py")
with open(tmp_file, "w", encoding="utf-8") as f:
f.write(code)
try: try:
result = subprocess.run(["python3", "-c", code], capture_output=True, text=True, timeout=10) result = subprocess.run(["python3", tmp_file], capture_output=True, text=True, timeout=10)
if result.returncode == -31: if result.returncode == -31:
return {"error": "Dangerous behavior detected."} return {"error": "Dangerous behavior detected."}
if result.stderr != "": if result.stderr != "":

View File

@ -1,6 +1,9 @@
import { RunCodeDto, RunCodeResponse } from 'src/sandbox/dto/create-sandbox.dto'; import { RunCodeDto, RunCodeResponse } from 'src/sandbox/dto/create-sandbox.dto';
import IsolatedVM, { ExternalCopy, Isolate, Reference } from 'isolated-vm'; import IsolatedVM, { ExternalCopy, Isolate, Reference } from 'isolated-vm';
import { mkdtemp, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { rmSync } from 'fs';
import { countToken } from './jsFn/tiktoken'; import { countToken } from './jsFn/tiktoken';
import { timeDelay } from './jsFn/delay'; import { timeDelay } from './jsFn/delay';
import { strToBase64 } from './jsFn/str2Base64'; import { strToBase64 } from './jsFn/str2Base64';
@ -9,7 +12,7 @@ import { createHmac } from './jsFn/crypto';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { pythonScript } from './constants'; import { pythonScript } from './constants';
const CustomLogStr = 'CUSTOM_LOG'; const CustomLogStr = 'CUSTOM_LOG';
const PythonScriptFileName = 'main.py';
export const runJsSandbox = async ({ export const runJsSandbox = async ({
code, code,
variables = {} variables = {}
@ -112,15 +115,16 @@ export const runPythonSandbox = async ({
code, code,
variables = {} variables = {}
}: RunCodeDto): Promise<RunCodeResponse> => { }: RunCodeDto): Promise<RunCodeResponse> => {
const tempDir = await mkdtemp(join(tmpdir(), 'python_script_tmp_'));
const mainCallCode = ` const mainCallCode = `
data = ${JSON.stringify({ code, variables })} data = ${JSON.stringify({ code, variables, tempDir })}
res = run_pythonCode(data) res = run_pythonCode(data)
print(json.dumps(res)) print(json.dumps(res))
`; `;
const fullCode = [pythonScript, mainCallCode].filter(Boolean).join('\n'); const fullCode = [pythonScript, mainCallCode].filter(Boolean).join('\n');
const { path: tempFilePath, cleanup } = await createTempFile(tempDir, fullCode);
const pythonProcess = spawn('python3', ['-u', '-c', fullCode]); const pythonProcess = spawn('python3', ['-u', tempFilePath]);
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
const stderrChunks: string[] = []; const stderrChunks: string[] = [];
@ -137,7 +141,9 @@ print(json.dumps(res))
} }
}); });
}); });
const stdout = await stdoutPromise; const stdout = await stdoutPromise.finally(() => {
cleanup();
});
try { try {
const parsedOutput = JSON.parse(stdout); const parsedOutput = JSON.parse(stdout);
@ -146,7 +152,10 @@ print(json.dumps(res))
} }
return { codeReturn: parsedOutput, log: '' }; return { codeReturn: parsedOutput, log: '' };
} catch (err) { } catch (err) {
if (stdout.includes('malformed node or string on line 1')) { if (
stdout.includes('malformed node or string on line 1') ||
stdout.includes('invalid syntax (<unknown>, line 1)')
) {
return Promise.reject(`The result should be a parsable variable, such as a list. ${stdout}`); return Promise.reject(`The result should be a parsable variable, such as a list. ${stdout}`);
} else if (stdout.includes('Unexpected end of JSON input')) { } else if (stdout.includes('Unexpected end of JSON input')) {
return Promise.reject(`Not allowed print or ${stdout}`); return Promise.reject(`Not allowed print or ${stdout}`);
@ -154,3 +163,24 @@ print(json.dumps(res))
return Promise.reject(`Run failed: ${err}`); return Promise.reject(`Run failed: ${err}`);
} }
}; };
// write full code into a tmp file
async function createTempFile(tempFileDirPath: string, context: string) {
const tempFilePath = join(tempFileDirPath, PythonScriptFileName);
try {
await writeFile(tempFilePath, context);
return {
path: tempFilePath,
cleanup: () => {
rmSync(tempFilePath);
rmSync(tempFileDirPath, {
recursive: true,
force: true
});
}
};
} catch (err) {
return Promise.reject(`write file err: ${err}`);
}
}