Skip to content

Commit 0c9e56c

Browse files
c121914yufforever14gggaaallleee
authored
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 <[email protected]> Co-authored-by: gggaaallleee <[email protected]>
1 parent 97a6c67 commit 0c9e56c

File tree

3 files changed

+52
-18
lines changed

3 files changed

+52
-18
lines changed

projects/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
该目录为 FastGPT 主项目。
44

55
- app fastgpt 核心应用。
6-
- sandbox 沙盒项目,用于运行工作流里的代码执行 (需求python环境为python:3.11,额外安装的包请于requirements.txt填写,同时注意个别包可能额外安装库(如pandas需要安装libffi))。
7-
- 新加入python包遇见超时或者权限拦截的问题(确定不是自己的语法问题),请进入docker容器内部执行以下指令:
6+
- sandbox 沙盒项目,用于运行工作流里的代码执行 (需求python环境为python:3.11,额外安装的包请于requirements.txt填写,在运行时会读取安装。
7+
8+
- 注意个别安装的包可能需要额外安装库(如pandas需要安装libffi))。
9+
10+
- 新加入python的包遇见超时或者权限拦截的问题(确定不是自己的语法问题),请进入docker容器内部执行以下指令:
811

912
```shell
1013
docker exec -it 《替换成容器名》 /bin/bash
1114
chmod -x testSystemCall.sh
1215
bash ./testSystemCall.sh
1316
```
14-
15-
然后将新的数组替换src下sandbox的constants.py中的SYSTEM_CALLS数组即可
1617

18+
然后将新的数组替换或追加到src下sandbox的constants.py中的SYSTEM_CALLS数组即可

projects/sandbox/src/sandbox/constants.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const pythonScript = `
2+
import os
23
import subprocess
34
import json
45
import ast
@@ -20,6 +21,7 @@ def extract_imports(code):
2021
seccomp_prefix = """
2122
from seccomp import *
2223
import sys
24+
import errno
2325
allowed_syscalls = [
2426
"syscall.SYS_ARCH_PRCTL", "syscall.SYS_BRK", "syscall.SYS_CLONE",
2527
"syscall.SYS_CLOSE", "syscall.SYS_EPOLL_CREATE1", "syscall.SYS_EXECVE",
@@ -99,22 +101,22 @@ def run_pythonCode(data:dict):
99101
variables = data["variables"]
100102
imports = "\\n".join(extract_imports(code))
101103
var_def = ""
102-
output_code = "res = main("
104+
output_code = "if __name__ == '__main__':\\n res = main("
103105
for k, v in variables.items():
104-
if isinstance(v, str):
105-
one_var = k + " = \\"" + v + "\\"\\n"
106-
else:
107-
one_var = k + " = " + str(v) + "\\n"
106+
one_var = f"{k} = {json.dumps(v)}\\n"
108107
var_def = var_def + one_var
109108
output_code = output_code + k + ", "
110109
if output_code[-1] == "(":
111110
output_code = output_code + ")\\n"
112111
else:
113112
output_code = output_code[:-2] + ")\\n"
114-
output_code = output_code + "print(res)"
113+
output_code = output_code + " print(res)"
115114
code = imports + "\\n" + seccomp_prefix + "\\n" + var_def + "\\n" + code + "\\n" + output_code
115+
tmp_file = os.path.join(data["tempDir"], "subProcess.py")
116+
with open(tmp_file, "w", encoding="utf-8") as f:
117+
f.write(code)
116118
try:
117-
result = subprocess.run(["python3", "-c", code], capture_output=True, text=True, timeout=10)
119+
result = subprocess.run(["python3", tmp_file], capture_output=True, text=True, timeout=10)
118120
if result.returncode == -31:
119121
return {"error": "Dangerous behavior detected."}
120122
if result.stderr != "":

projects/sandbox/src/sandbox/utils.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { RunCodeDto, RunCodeResponse } from 'src/sandbox/dto/create-sandbox.dto';
22
import IsolatedVM, { ExternalCopy, Isolate, Reference } from 'isolated-vm';
3-
3+
import { mkdtemp, writeFile } from 'fs/promises';
4+
import { tmpdir } from 'os';
5+
import { join } from 'path';
6+
import { rmSync } from 'fs';
47
import { countToken } from './jsFn/tiktoken';
58
import { timeDelay } from './jsFn/delay';
69
import { strToBase64 } from './jsFn/str2Base64';
@@ -9,7 +12,7 @@ import { createHmac } from './jsFn/crypto';
912
import { spawn } from 'child_process';
1013
import { pythonScript } from './constants';
1114
const CustomLogStr = 'CUSTOM_LOG';
12-
15+
const PythonScriptFileName = 'main.py';
1316
export const runJsSandbox = async ({
1417
code,
1518
variables = {}
@@ -112,15 +115,16 @@ export const runPythonSandbox = async ({
112115
code,
113116
variables = {}
114117
}: RunCodeDto): Promise<RunCodeResponse> => {
118+
const tempDir = await mkdtemp(join(tmpdir(), 'python_script_tmp_'));
115119
const mainCallCode = `
116-
data = ${JSON.stringify({ code, variables })}
120+
data = ${JSON.stringify({ code, variables, tempDir })}
117121
res = run_pythonCode(data)
118122
print(json.dumps(res))
119123
`;
120124

121125
const fullCode = [pythonScript, mainCallCode].filter(Boolean).join('\n');
122-
123-
const pythonProcess = spawn('python3', ['-u', '-c', fullCode]);
126+
const { path: tempFilePath, cleanup } = await createTempFile(tempDir, fullCode);
127+
const pythonProcess = spawn('python3', ['-u', tempFilePath]);
124128

125129
const stdoutChunks: string[] = [];
126130
const stderrChunks: string[] = [];
@@ -137,7 +141,9 @@ print(json.dumps(res))
137141
}
138142
});
139143
});
140-
const stdout = await stdoutPromise;
144+
const stdout = await stdoutPromise.finally(() => {
145+
cleanup();
146+
});
141147

142148
try {
143149
const parsedOutput = JSON.parse(stdout);
@@ -146,11 +152,35 @@ print(json.dumps(res))
146152
}
147153
return { codeReturn: parsedOutput, log: '' };
148154
} catch (err) {
149-
if (stdout.includes('malformed node or string on line 1')) {
155+
if (
156+
stdout.includes('malformed node or string on line 1') ||
157+
stdout.includes('invalid syntax (<unknown>, line 1)')
158+
) {
150159
return Promise.reject(`The result should be a parsable variable, such as a list. ${stdout}`);
151160
} else if (stdout.includes('Unexpected end of JSON input')) {
152161
return Promise.reject(`Not allowed print or ${stdout}`);
153162
}
154163
return Promise.reject(`Run failed: ${err}`);
155164
}
156165
};
166+
167+
// write full code into a tmp file
168+
async function createTempFile(tempFileDirPath: string, context: string) {
169+
const tempFilePath = join(tempFileDirPath, PythonScriptFileName);
170+
171+
try {
172+
await writeFile(tempFilePath, context);
173+
return {
174+
path: tempFilePath,
175+
cleanup: () => {
176+
rmSync(tempFilePath);
177+
rmSync(tempFileDirPath, {
178+
recursive: true,
179+
force: true
180+
});
181+
}
182+
};
183+
} catch (err) {
184+
return Promise.reject(`write file err: ${err}`);
185+
}
186+
}

0 commit comments

Comments
 (0)