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:
parent
97a6c6749a
commit
0c9e56c1ee
@ -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数组即可
|
||||||
|
|||||||
@ -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 != "":
|
||||||
|
|||||||
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user