Commit ffc8ef45 by Archer Committed by GitHub

fix: harden code sandbox resource limits (#6997)

parent 484a7654
...@@ -185,17 +185,27 @@ services: ...@@ -185,17 +185,27 @@ services:
networks: networks:
- fastgpt - fastgpt
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -203,7 +213,7 @@ services: ...@@ -203,7 +213,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -185,17 +185,27 @@ services: ...@@ -185,17 +185,27 @@ services:
networks: networks:
- fastgpt - fastgpt
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -203,7 +213,7 @@ services: ...@@ -203,7 +213,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -185,17 +185,27 @@ services: ...@@ -185,17 +185,27 @@ services:
networks: networks:
- fastgpt - fastgpt
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -203,7 +213,7 @@ services: ...@@ -203,7 +213,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -204,17 +204,27 @@ ${{vec.db}} ...@@ -204,17 +204,27 @@ ${{vec.db}}
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -222,7 +232,7 @@ ${{vec.db}} ...@@ -222,7 +232,7 @@ ${{vec.db}}
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -266,9 +266,11 @@ These variables are loaded and validated by `projects/code-sandbox/src/env.ts`. ...@@ -266,9 +266,11 @@ These variables are loaded and validated by `projects/code-sandbox/src/env.ts`.
| `SANDBOX_PORT` | `3000` | Code Sandbox listening port. | | `SANDBOX_PORT` | `3000` | Code Sandbox listening port. |
| `SANDBOX_TOKEN` | Empty | Bearer token for the `/sandbox` endpoint. Empty disables API authentication. It only allows printable ASCII characters and cannot contain spaces. | | `SANDBOX_TOKEN` | Empty | Bearer token for the `/sandbox` endpoint. Empty disables API authentication. It only allows printable ASCII characters and cannot contain spaces. |
| `SANDBOX_POOL_SIZE` | `20` | Number of pre-warmed JS/Python workers, from `1` to `100`. | | `SANDBOX_POOL_SIZE` | `20` | Number of pre-warmed JS/Python workers, from `1` to `100`. |
| `SANDBOX_API_MAX_BODY_MB` | `8` | Maximum `/sandbox` API JSON body size, including `variables`, in MB. Range: `1` to `100`. |
| `SANDBOX_MAX_TIMEOUT` | `60000` | Timeout for one code execution, in milliseconds. Range: `1000` to `600000`. | | `SANDBOX_MAX_TIMEOUT` | `60000` | Timeout for one code execution, in milliseconds. Range: `1000` to `600000`. |
| `SANDBOX_MAX_MEMORY_MB` | `256` | Maximum memory for one sandbox, in MB. Range: `32` to `4096`. | | `SANDBOX_MAX_MEMORY_MB` | `256` | Maximum memory for one sandbox, in MB. Range: `32` to `4096`. The runtime reserves an extra `50` MB for overhead. |
| `CHECK_INTERNAL_IP` | `false` | Whether internal IP checks are enabled for sandbox network requests. | | `SANDBOX_MAX_OUTPUT_MB` | `10` | Maximum output JSON size for one code execution, including return values and logs, in MB. Range: `1` to `100`. |
| `CHECK_INTERNAL_IP` | `true` | Whether internal IP checks are enabled for sandbox network requests. |
| `SANDBOX_REQUEST_MAX_COUNT` | `30` | Maximum number of network requests allowed during one code execution. Range: `1` to `1000`. | | `SANDBOX_REQUEST_MAX_COUNT` | `30` | Maximum number of network requests allowed during one code execution. Range: `1` to `1000`. |
| `SANDBOX_REQUEST_TIMEOUT` | `60000` | Timeout for one network request from inside the sandbox, in milliseconds. Range: `1000` to `300000`. | | `SANDBOX_REQUEST_TIMEOUT` | `60000` | Timeout for one network request from inside the sandbox, in milliseconds. Range: `1000` to `300000`. |
| `SANDBOX_REQUEST_MAX_RESPONSE_MB` | `10` | Maximum response body size for one sandbox network request, in MB. Range: `1` to `100`. | | `SANDBOX_REQUEST_MAX_RESPONSE_MB` | `10` | Maximum response body size for one sandbox network request, in MB. Range: `1` to `100`. |
......
...@@ -266,9 +266,11 @@ description: projects/app、projects/code-sandbox 与 pro/admin 环境变量说 ...@@ -266,9 +266,11 @@ description: projects/app、projects/code-sandbox 与 pro/admin 环境变量说
| `SANDBOX_PORT` | `3000` | Code Sandbox 服务监听端口。 | | `SANDBOX_PORT` | `3000` | Code Sandbox 服务监听端口。 |
| `SANDBOX_TOKEN` | 空 | `/sandbox` 接口 Bearer Token;为空时不启用接口认证。仅允许 ASCII 可打印字符且不能包含空格。 | | `SANDBOX_TOKEN` | 空 | `/sandbox` 接口 Bearer Token;为空时不启用接口认证。仅允许 ASCII 可打印字符且不能包含空格。 |
| `SANDBOX_POOL_SIZE` | `20` | JS/Python 预热 worker 数量,范围 `1` 到 `100`。 | | `SANDBOX_POOL_SIZE` | `20` | JS/Python 预热 worker 数量,范围 `1` 到 `100`。 |
| `SANDBOX_API_MAX_BODY_MB` | `8` | `/sandbox` API JSON 请求体总大小上限,包含 `variables`,单位 MB,范围 `1` 到 `100`。 |
| `SANDBOX_MAX_TIMEOUT` | `60000` | 单次代码执行超时时间,单位毫秒,范围 `1000` 到 `600000`。 | | `SANDBOX_MAX_TIMEOUT` | `60000` | 单次代码执行超时时间,单位毫秒,范围 `1000` 到 `600000`。 |
| `SANDBOX_MAX_MEMORY_MB` | `256` | 单个沙箱最大内存,单位 MB,范围 `32` 到 `4096`。 | | `SANDBOX_MAX_MEMORY_MB` | `256` | 单个沙箱最大内存,单位 MB,范围 `32` 到 `4096`;运行时会额外预留 `50` MB 开销。 |
| `CHECK_INTERNAL_IP` | `false` | 是否在沙箱网络请求中启用内网 IP 检查。 | | `SANDBOX_MAX_OUTPUT_MB` | `10` | 单次代码执行输出 JSON 大小上限,包含返回值和日志,单位 MB,范围 `1` 到 `100`。 |
| `CHECK_INTERNAL_IP` | `true` | 是否在沙箱网络请求中启用内网 IP 检查。 |
| `SANDBOX_REQUEST_MAX_COUNT` | `30` | 单次代码执行允许发起的最大网络请求数,范围 `1` 到 `1000`。 | | `SANDBOX_REQUEST_MAX_COUNT` | `30` | 单次代码执行允许发起的最大网络请求数,范围 `1` 到 `1000`。 |
| `SANDBOX_REQUEST_TIMEOUT` | `60000` | 沙箱内单次网络请求超时时间,单位毫秒,范围 `1000` 到 `300000`。 | | `SANDBOX_REQUEST_TIMEOUT` | `60000` | 沙箱内单次网络请求超时时间,单位毫秒,范围 `1000` 到 `300000`。 |
| `SANDBOX_REQUEST_MAX_RESPONSE_MB` | `10` | 沙箱内单次网络响应体最大大小,单位 MB,范围 `1` 到 `100`。 | | `SANDBOX_REQUEST_MAX_RESPONSE_MB` | `10` | 沙箱内单次网络响应体最大大小,单位 MB,范围 `1` 到 `100`。 |
......
...@@ -5,10 +5,40 @@ description: 'FastGPT V4.15.0-beta3 Release Notes' ...@@ -5,10 +5,40 @@ description: 'FastGPT V4.15.0-beta3 Release Notes'
## 🚀 New Features ## 🚀 New Features
1. Multimodal models now support audio and video input.
2. Shared links and portal pages now support language switching, and no longer force browser-language auto switching.
## 🔧 Environment Variable Changes
Code Sandbox adds security-related environment variables such as `SANDBOX_API_MAX_BODY_MB` and `SANDBOX_MAX_OUTPUT_MB`. The full defaults are listed below:
| Variable | Default | Description |
| --------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
| `SANDBOX_API_MAX_BODY_MB` | `8` | Maximum `/sandbox` API JSON body size, including `variables`, in MB. |
| `SANDBOX_MAX_OUTPUT_MB` | `10` | Maximum output JSON size for one code execution, including return values and logs, in MB. |
| `CHECK_INTERNAL_IP` | `true` | Enables internal IP checks for sandbox network requests by default to reduce SSRF risk. |
| `SANDBOX_MAX_TIMEOUT` | `60000` | Timeout for one code execution, in milliseconds. |
| `SANDBOX_MAX_MEMORY_MB` | `256` | Memory limit for one sandbox, in MB. The runtime reserves an extra `50` MB for overhead. |
| `SANDBOX_POOL_SIZE` | `20` | Number of pre-warmed JS/Python workers. |
| `SANDBOX_REQUEST_MAX_COUNT` | `30` | Maximum number of network requests allowed during one code execution. |
| `SANDBOX_REQUEST_TIMEOUT` | `60000` | Timeout for one network request from inside the sandbox, in milliseconds. |
| `SANDBOX_REQUEST_MAX_RESPONSE_MB` | `10` | Maximum response body size for one sandbox network request, in MB. |
| `SANDBOX_REQUEST_MAX_BODY_MB` | `5` | Maximum request body size for one sandbox network request, in MB. |
## ⚙️ Improvements ## ⚙️ Improvements
1. Improved styles for Skill module dialogs.
2. Improved Skill list API performance.
3. Improved workflow node name and description inputs.
## 🐛 Bug Fixes ## 🐛 Bug Fixes
1. Adapted TTS audio playback to the latest OpenAI SDK to avoid errors.
## 🛠️ Code Improvements ## 🛠️ Code Improvements
1. Updated the token calculation dependency to improve performance. 1. Updated the token calculation dependency to improve performance.
2. Rewrote dialog-related code with more modular structure.
3. Improved unit test performance, reducing full runs from about 10 minutes to 5 minutes.
4. Upgraded to TypeScript 6.
5. Improved GitHub Actions security.
...@@ -8,6 +8,23 @@ description: 'FastGPT V4.15.0-beta3 更新说明' ...@@ -8,6 +8,23 @@ description: 'FastGPT V4.15.0-beta3 更新说明'
1. 多模态模型支持音视频输入。 1. 多模态模型支持音视频输入。
2. 分享链接/门户页,支持语言切换,不再强制自动识别浏览器语言切换。 2. 分享链接/门户页,支持语言切换,不再强制自动识别浏览器语言切换。
## 🔧 环境变量变更
Code Sandbox 新增 `SANDBOX_API_MAX_BODY_MB`、`SANDBOX_MAX_OUTPUT_MB` 等安全相关环境变量;完整默认值如下:
| 变量 | 默认值 | 说明 |
| --------------------------------- | ------- | ----------------------------------------------------------------- |
| `SANDBOX_API_MAX_BODY_MB` | `8` | `/sandbox` API JSON 请求体总大小上限,包含 `variables`,单位 MB。 |
| `SANDBOX_MAX_OUTPUT_MB` | `10` | 单次代码执行输出 JSON 大小上限,包含返回值和日志,单位 MB。 |
| `CHECK_INTERNAL_IP` | `true` | 沙箱网络请求默认开启内网 IP 检查,降低 SSRF 风险。 |
| `SANDBOX_MAX_TIMEOUT` | `60000` | 单次代码执行超时时间,单位毫秒。 |
| `SANDBOX_MAX_MEMORY_MB` | `256` | 单个沙箱内存上限,单位 MB;运行时会额外预留 `50` MB 开销。 |
| `SANDBOX_POOL_SIZE` | `20` | JS/Python 预热 worker 数量。 |
| `SANDBOX_REQUEST_MAX_COUNT` | `30` | 单次代码执行允许发起的最大网络请求数。 |
| `SANDBOX_REQUEST_TIMEOUT` | `60000` | 沙箱内单次网络请求超时时间,单位毫秒。 |
| `SANDBOX_REQUEST_MAX_RESPONSE_MB` | `10` | 沙箱内单次网络响应体最大大小,单位 MB。 |
| `SANDBOX_REQUEST_MAX_BODY_MB` | `5` | 沙箱内单次网络请求体最大大小,单位 MB。 |
## ⚙️ 优化 ## ⚙️ 优化
1. Skill 模块相关弹窗样式。 1. Skill 模块相关弹窗样式。
......
...@@ -261,17 +261,27 @@ services: ...@@ -261,17 +261,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -279,7 +289,7 @@ services: ...@@ -279,7 +289,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -239,17 +239,27 @@ services: ...@@ -239,17 +239,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -257,7 +267,7 @@ services: ...@@ -257,7 +267,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -223,17 +223,27 @@ services: ...@@ -223,17 +223,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -241,7 +251,7 @@ services: ...@@ -241,7 +251,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -221,17 +221,27 @@ services: ...@@ -221,17 +221,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -239,7 +249,7 @@ services: ...@@ -239,7 +249,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -226,17 +226,27 @@ services: ...@@ -226,17 +226,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -244,7 +254,7 @@ services: ...@@ -244,7 +254,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -205,17 +205,27 @@ services: ...@@ -205,17 +205,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -223,7 +233,7 @@ services: ...@@ -223,7 +233,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -261,17 +261,27 @@ services: ...@@ -261,17 +261,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -279,7 +289,7 @@ services: ...@@ -279,7 +289,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -239,17 +239,27 @@ services: ...@@ -239,17 +239,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -257,7 +267,7 @@ services: ...@@ -257,7 +267,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -223,17 +223,27 @@ services: ...@@ -223,17 +223,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -241,7 +251,7 @@ services: ...@@ -241,7 +251,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -221,17 +221,27 @@ services: ...@@ -221,17 +221,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -239,7 +249,7 @@ services: ...@@ -239,7 +249,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -226,17 +226,27 @@ services: ...@@ -226,17 +226,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -244,7 +254,7 @@ services: ...@@ -244,7 +254,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -205,17 +205,27 @@ services: ...@@ -205,17 +205,27 @@ services:
networks: networks:
- codesandbox - codesandbox
restart: always restart: always
read_only: true
tmpfs:
- /tmp:size=128m,noexec,nosuid,nodev
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment: environment:
<<: [*x-log-config, *x-no-proxy-config] <<: [*x-log-config, *x-no-proxy-config]
LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox
SANDBOX_TOKEN: *x-code-sandbox-token SANDBOX_TOKEN: *x-code-sandbox-token
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB: 8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT: 60000 SANDBOX_MAX_TIMEOUT: 60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB: 256 SANDBOX_MAX_MEMORY_MB: 256
SANDBOX_MAX_OUTPUT_MB: 10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -223,7 +233,7 @@ services: ...@@ -223,7 +233,7 @@ services:
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP: false CHECK_INTERNAL_IP: true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT: 30 SANDBOX_REQUEST_MAX_COUNT: 30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -40,6 +40,10 @@ export class CodeSandbox { ...@@ -40,6 +40,10 @@ export class CodeSandbox {
return response.data; return response.data;
}, },
(error) => { (error) => {
const message = error?.response?.data?.message;
if (typeof message === 'string' && message) {
return Promise.reject(new Error(message));
}
return Promise.reject(error); return Promise.reject(error);
} }
); );
......
...@@ -14,12 +14,16 @@ LOG_OTEL_SERVICE_NAME=fastgpt-client ...@@ -14,12 +14,16 @@ LOG_OTEL_SERVICE_NAME=fastgpt-client
LOG_OTEL_URL=http://localhost:4318/v1/logs LOG_OTEL_URL=http://localhost:4318/v1/logs
# ===== Resource Limits ===== # ===== Resource Limits =====
# Maximum API JSON body size (MB), including variables
SANDBOX_API_MAX_BODY_MB=8
# Execution timeout per request (ms) # Execution timeout per request (ms)
SANDBOX_MAX_TIMEOUT=60000 SANDBOX_MAX_TIMEOUT=60000
# Maximum allowed memory per user code execution (MB) # Maximum allowed memory per user code execution (MB)
# Note: System automatically adds 50MB for runtime overhead # Note: System automatically adds 50MB for runtime overhead
# Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB # Actual process limit = SANDBOX_MAX_MEMORY_MB + 50MB
SANDBOX_MAX_MEMORY_MB=256 SANDBOX_MAX_MEMORY_MB=256
# Maximum output JSON size per execution (MB), including return value and logs
SANDBOX_MAX_OUTPUT_MB=10
# ===== Process Pool ===== # ===== Process Pool =====
# Number of pre-warmed worker processes (JS + Python) # Number of pre-warmed worker processes (JS + Python)
...@@ -27,7 +31,7 @@ SANDBOX_POOL_SIZE=20 ...@@ -27,7 +31,7 @@ SANDBOX_POOL_SIZE=20
# ===== Network Request Limits ===== # ===== Network Request Limits =====
# Whether to check if the request is to a private network # Whether to check if the request is to a private network
CHECK_INTERNAL_IP=false CHECK_INTERNAL_IP=true
# Maximum number of HTTP requests per execution # Maximum number of HTTP requests per execution
SANDBOX_REQUEST_MAX_COUNT=30 SANDBOX_REQUEST_MAX_COUNT=30
# Timeout for each outbound HTTP request (ms) # Timeout for each outbound HTTP request (ms)
......
...@@ -73,6 +73,10 @@ USER sandbox ...@@ -73,6 +73,10 @@ USER sandbox
ENV NODE_ENV=production ENV NODE_ENV=production
ENV SANDBOX_PORT=3000 ENV SANDBOX_PORT=3000
ENV HOME=/tmp
ENV XDG_CACHE_HOME=/tmp/.cache
ENV MPLCONFIGDIR=/tmp/matplotlib
ENV PYTHONDONTWRITEBYTECODE=1
EXPOSE 3000 EXPOSE 3000
......
...@@ -145,13 +145,16 @@ docker run -p 3000:3000 \ ...@@ -145,13 +145,16 @@ docker run -p 3000:3000 \
| 变量 | 说明 | 默认值 | | 变量 | 说明 | 默认值 |
|------|------|--------| |------|------|--------|
| `SANDBOX_API_MAX_BODY_MB` | API JSON 请求体总大小上限(包含 variables) | `8` |
| `SANDBOX_MAX_TIMEOUT` | 超时上限(ms),请求不可超过此值 | `60000` | | `SANDBOX_MAX_TIMEOUT` | 超时上限(ms),请求不可超过此值 | `60000` |
| `SANDBOX_MAX_MEMORY_MB` | 内存上限(MB) | `256` | | `SANDBOX_MAX_MEMORY_MB` | 内存上限(MB) | `256` |
| `SANDBOX_MAX_OUTPUT_MB` | 单次执行输出 JSON 大小上限(包含返回值和日志) | `10` |
### 网络请求限制 ### 网络请求限制
| 变量 | 说明 | 默认值 | | 变量 | 说明 | 默认值 |
|------|------|--------| |------|------|--------|
| `CHECK_INTERNAL_IP` | 是否阻止访问内网、回环、链路本地等地址 | `true` |
| `SANDBOX_REQUEST_MAX_COUNT` | 单次执行最大 HTTP 请求数 | `30` | | `SANDBOX_REQUEST_MAX_COUNT` | 单次执行最大 HTTP 请求数 | `30` |
| `SANDBOX_REQUEST_TIMEOUT` | 单次 HTTP 请求超时(ms) | `60000` | | `SANDBOX_REQUEST_TIMEOUT` | 单次 HTTP 请求超时(ms) | `60000` |
| `SANDBOX_REQUEST_MAX_RESPONSE_MB` | 最大响应体大小(MB) | `10` | | `SANDBOX_REQUEST_MAX_RESPONSE_MB` | 最大响应体大小(MB) | `10` |
...@@ -214,6 +217,7 @@ SANDBOX_JS_ALLOWED_MODULES=lodash,dayjs,moment,uuid,crypto-js,qs,url,querystring ...@@ -214,6 +217,7 @@ SANDBOX_JS_ALLOWED_MODULES=lodash,dayjs,moment,uuid,crypto-js,qs,url,querystring
- 只添加纯计算类的包,不要添加有网络/文件系统/子进程能力的包 - 只添加纯计算类的包,不要添加有网络/文件系统/子进程能力的包
- 包会被打入 Docker 镜像,注意体积 - 包会被打入 Docker 镜像,注意体积
- 网络请求统一走 `SystemHelper.httpRequest()`,不要放行 `axios``node-fetch` 等网络库 - 网络请求统一走 `SystemHelper.httpRequest()`,不要放行 `axios``node-fetch` 等网络库
- 如果显式放行 `child_process``worker_threads``cluster`,worker 会在每次任务后回收,以清理潜在后台执行残留
## 添加 Python 包 ## 添加 Python 包
...@@ -233,7 +237,7 @@ your-new-package ...@@ -233,7 +237,7 @@ your-new-package
2. **加入白名单**(环境变量 `SANDBOX_PYTHON_ALLOWED_MODULES`): 2. **加入白名单**(环境变量 `SANDBOX_PYTHON_ALLOWED_MODULES`):
在逗号分隔列表中添加包名。如果新包依赖了黑名单中的模块(如 `os`),标准库路径的间接导入会自动放行,无需额外配置 在逗号分隔列表中添加包名。用户代码能否直接 import 某个模块完全由 `SANDBOX_PYTHON_ALLOWED_MODULES` 控制;第三方包和标准库内部依赖会按调用栈放行,避免误伤包自身初始化
3. **重新构建 Docker 镜像** 3. **重新构建 Docker 镜像**
...@@ -241,7 +245,8 @@ your-new-package ...@@ -241,7 +245,8 @@ your-new-package
- Python 的模块黑名单通过 `__import__` 拦截实现,只拦截用户代码的直接 import - Python 的模块黑名单通过 `__import__` 拦截实现,只拦截用户代码的直接 import
- 标准库和第三方包的内部间接 import 不受影响 - 标准库和第三方包的内部间接 import 不受影响
- 危险模块(`os``sys``subprocess``socket` 等)始终被拦截 - 默认白名单不包含 `os``sys``subprocess``socket` 等高危模块;如果显式加入环境变量白名单,用户代码会按配置获得对应能力
- 如果显式放行 `subprocess``multiprocessing``threading``concurrent`,worker 会在每次任务后回收,以清理潜在后台执行残留
## 安全机制 ## 安全机制
...@@ -255,7 +260,7 @@ your-new-package ...@@ -255,7 +260,7 @@ your-new-package
### Python ### Python
- `__import__` 黑名单拦截:用户代码无法 import 危险模块(`os``sys``subprocess` 等) - `__import__` 白名单控制:默认不允许用户代码 import `os``sys``subprocess` 等高危模块;显式加入 `SANDBOX_PYTHON_ALLOWED_MODULES` 后按配置放行
- `exec()`/`eval()` 内的 import 同样被拦截(基于调用栈帧检测) - `exec()`/`eval()` 内的 import 同样被拦截(基于调用栈帧检测)
- `builtins.__import__` 通过代理对象保护,用户无法覆盖 - `builtins.__import__` 通过代理对象保护,用户无法覆盖
- `signal.SIGALRM` 超时保护 - `signal.SIGALRM` 超时保护
......
...@@ -49,11 +49,13 @@ export const env = createEnv({ ...@@ -49,11 +49,13 @@ export const env = createEnv({
SANDBOX_POOL_SIZE: IntSchema.min(1).max(100).default(20), SANDBOX_POOL_SIZE: IntSchema.min(1).max(100).default(20),
// ===== 资源限制 ===== // ===== 资源限制 =====
SANDBOX_API_MAX_BODY_MB: IntSchema.min(1).max(100).default(8),
SANDBOX_MAX_TIMEOUT: IntSchema.min(1000).max(600000).default(60000), SANDBOX_MAX_TIMEOUT: IntSchema.min(1000).max(600000).default(60000),
SANDBOX_MAX_MEMORY_MB: IntSchema.min(32).max(4096).default(256), SANDBOX_MAX_MEMORY_MB: IntSchema.min(32).max(4096).default(256),
SANDBOX_MAX_OUTPUT_MB: IntSchema.min(1).max(100).default(10),
// ===== 网络请求限制 ===== // ===== 网络请求限制 =====
CHECK_INTERNAL_IP: BoolSchema.default(false), CHECK_INTERNAL_IP: BoolSchema.default(true),
SANDBOX_REQUEST_MAX_COUNT: IntSchema.min(1).max(1000).default(30), SANDBOX_REQUEST_MAX_COUNT: IntSchema.min(1).max(1000).default(30),
SANDBOX_REQUEST_TIMEOUT: IntSchema.min(1000).max(300000).default(60000), SANDBOX_REQUEST_TIMEOUT: IntSchema.min(1000).max(300000).default(60000),
SANDBOX_REQUEST_MAX_RESPONSE_MB: IntSchema.min(1).max(100).default(10), SANDBOX_REQUEST_MAX_RESPONSE_MB: IntSchema.min(1).max(100).default(10),
......
import { env } from './env'; import { env } from './env';
import { Hono } from 'hono'; import { Hono, type Context } from 'hono';
import { bearerAuth } from 'hono/bearer-auth'; import { bearerAuth } from 'hono/bearer-auth';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { z } from 'zod'; import { z } from 'zod';
...@@ -13,6 +13,62 @@ await configureLogger(); ...@@ -13,6 +13,62 @@ await configureLogger();
const serverLogger = getLogger(LogCategories.MODULE.SANDBOX.SERVER); const serverLogger = getLogger(LogCategories.MODULE.SANDBOX.SERVER);
const apiLogger = getLogger(LogCategories.MODULE.SANDBOX.API); const apiLogger = getLogger(LogCategories.MODULE.SANDBOX.API);
const maxApiBodyBytes = env.SANDBOX_API_MAX_BODY_MB * 1024 * 1024;
class ApiBodyError extends Error {
constructor(
message: string,
readonly status: 400 | 413
) {
super(message);
}
}
/**
* 流式读取并限制 API JSON body 总大小。
*
* `c.req.json()` 会先把完整 body 读入内存;这里在进入 JSON.parse/zod 前按字节数
* 截断,防止攻击者通过超大的 variables 字段绕过 code 字段长度限制造成内存压力。
*/
async function readLimitedJsonBody(c: Context): Promise<unknown> {
const contentLength = Number(c.req.header('content-length') || 0);
if (Number.isFinite(contentLength) && contentLength > maxApiBodyBytes) {
throw new ApiBodyError(`Request body too large, max ${env.SANDBOX_API_MAX_BODY_MB}MB`, 413);
}
const body = c.req.raw.body;
if (!body) {
throw new ApiBodyError('Request body is empty', 400);
}
const reader = body.getReader();
const decoder = new TextDecoder();
let size = 0;
let text = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > maxApiBodyBytes) {
await reader.cancel();
throw new ApiBodyError(`Request body too large, max ${env.SANDBOX_API_MAX_BODY_MB}MB`, 413);
}
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
} catch (err) {
if (err instanceof ApiBodyError) throw err;
throw new ApiBodyError(getErrText(err, 'Invalid request body'), 400);
}
try {
return JSON.parse(text);
} catch (err) {
throw new ApiBodyError(`Invalid JSON body: ${getErrText(err)}`, 400);
}
}
/** 请求体校验 schema */ /** 请求体校验 schema */
const executeSchema = z.object({ const executeSchema = z.object({
...@@ -106,7 +162,7 @@ if (env.SANDBOX_TOKEN) { ...@@ -106,7 +162,7 @@ if (env.SANDBOX_TOKEN) {
/** JS 执行 */ /** JS 执行 */
app.post('/sandbox/js', async (c) => { app.post('/sandbox/js', async (c) => {
try { try {
const raw = await c.req.json(); const raw = await readLimitedJsonBody(c);
const parsed = executeSchema.safeParse(raw); const parsed = executeSchema.safeParse(raw);
if (!parsed.success) { if (!parsed.success) {
return c.json( return c.json(
...@@ -120,17 +176,21 @@ app.post('/sandbox/js', async (c) => { ...@@ -120,17 +176,21 @@ app.post('/sandbox/js', async (c) => {
const result = await jsPool.execute(parsed.data as ExecuteOptions); const result = await jsPool.execute(parsed.data as ExecuteOptions);
return c.json(result); return c.json(result);
} catch (err: any) { } catch (err: any) {
return c.json({ const status = err instanceof ApiBodyError ? err.status : 200;
return c.json(
{
success: false, success: false,
message: getErrText(err) message: getErrText(err)
}); },
status
);
} }
}); });
/** Python 执行 */ /** Python 执行 */
app.post('/sandbox/python', async (c) => { app.post('/sandbox/python', async (c) => {
try { try {
const raw = await c.req.json(); const raw = await readLimitedJsonBody(c);
const parsed = executeSchema.safeParse(raw); const parsed = executeSchema.safeParse(raw);
if (!parsed.success) { if (!parsed.success) {
return c.json( return c.json(
...@@ -144,10 +204,14 @@ app.post('/sandbox/python', async (c) => { ...@@ -144,10 +204,14 @@ app.post('/sandbox/python', async (c) => {
const result = await pythonPool.execute(parsed.data as ExecuteOptions); const result = await pythonPool.execute(parsed.data as ExecuteOptions);
return c.json(result); return c.json(result);
} catch (err: any) { } catch (err: any) {
return c.json({ const status = err instanceof ApiBodyError ? err.status : 200;
return c.json(
{
success: false, success: false,
message: getErrText(err) message: getErrText(err)
}); },
status
);
} }
}); });
......
...@@ -15,6 +15,18 @@ const isCompiled = import.meta.url.endsWith('.js'); ...@@ -15,6 +15,18 @@ const isCompiled = import.meta.url.endsWith('.js');
const WORKER_SCRIPT = join(__dirname, isCompiled ? 'worker.js' : 'worker.ts'); const WORKER_SCRIPT = join(__dirname, isCompiled ? 'worker.js' : 'worker.ts');
const SPAWN_RUNTIME = isCompiled ? 'node' : 'tsx'; const SPAWN_RUNTIME = isCompiled ? 'node' : 'tsx';
const RECYCLE_AFTER_TASK_MODULES = new Set([
'child_process',
'node:child_process',
'cluster',
'node:cluster',
'worker_threads',
'node:worker_threads'
]);
function shouldRecycleAfterTask(allowedModules: readonly string[]): boolean {
return allowedModules.some((moduleName) => RECYCLE_AFTER_TASK_MODULES.has(moduleName));
}
export class ProcessPool extends BaseProcessPool { export class ProcessPool extends BaseProcessPool {
constructor(poolSize?: number) { constructor(poolSize?: number) {
...@@ -22,7 +34,8 @@ export class ProcessPool extends BaseProcessPool { ...@@ -22,7 +34,8 @@ export class ProcessPool extends BaseProcessPool {
name: 'JS', name: 'JS',
workerScript: WORKER_SCRIPT, workerScript: WORKER_SCRIPT,
spawnCommand: (script) => `exec ${SPAWN_RUNTIME} ${script}`, spawnCommand: (script) => `exec ${SPAWN_RUNTIME} ${script}`,
allowedModules: env.SANDBOX_JS_ALLOWED_MODULES allowedModules: env.SANDBOX_JS_ALLOWED_MODULES,
recycleAfterTask: shouldRecycleAfterTask(env.SANDBOX_JS_ALLOWED_MODULES)
}); });
} }
} }
...@@ -10,6 +10,16 @@ import { BaseProcessPool } from './base-process-pool'; ...@@ -10,6 +10,16 @@ import { BaseProcessPool } from './base-process-pool';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const WORKER_SCRIPT = join(__dirname, 'worker.py'); const WORKER_SCRIPT = join(__dirname, 'worker.py');
const RECYCLE_AFTER_TASK_MODULES = new Set([
'subprocess',
'multiprocessing',
'threading',
'concurrent'
]);
function shouldRecycleAfterTask(allowedModules: readonly string[]): boolean {
return allowedModules.some((moduleName) => RECYCLE_AFTER_TASK_MODULES.has(moduleName));
}
export class PythonProcessPool extends BaseProcessPool { export class PythonProcessPool extends BaseProcessPool {
constructor(poolSize?: number) { constructor(poolSize?: number) {
...@@ -17,7 +27,8 @@ export class PythonProcessPool extends BaseProcessPool { ...@@ -17,7 +27,8 @@ export class PythonProcessPool extends BaseProcessPool {
name: 'Python', name: 'Python',
workerScript: WORKER_SCRIPT, workerScript: WORKER_SCRIPT,
spawnCommand: (script) => `exec python3 -u ${script}`, spawnCommand: (script) => `exec python3 -u ${script}`,
allowedModules: env.SANDBOX_PYTHON_ALLOWED_MODULES allowedModules: env.SANDBOX_PYTHON_ALLOWED_MODULES,
recycleAfterTask: shouldRecycleAfterTask(env.SANDBOX_PYTHON_ALLOWED_MODULES)
}); });
} }
} }
...@@ -64,6 +64,7 @@ _REQUEST_LIMITS = { ...@@ -64,6 +64,7 @@ _REQUEST_LIMITS = {
'timeout': 60, 'timeout': 60,
'max_response_size': 10 * 1024 * 1024, 'max_response_size': 10 * 1024 * 1024,
'max_request_body_size': 5 * 1024 * 1024, 'max_request_body_size': 5 * 1024 * 1024,
'max_output_size': 10 * 1024 * 1024,
'allowed_protocols': ['http:', 'https:'] 'allowed_protocols': ['http:', 'https:']
} }
...@@ -80,6 +81,8 @@ def _init_request_limits(limits): ...@@ -80,6 +81,8 @@ def _init_request_limits(limits):
_REQUEST_LIMITS['max_response_size'] = limits['maxResponseSize'] _REQUEST_LIMITS['max_response_size'] = limits['maxResponseSize']
if 'maxRequestBodySize' in limits: if 'maxRequestBodySize' in limits:
_REQUEST_LIMITS['max_request_body_size'] = limits['maxRequestBodySize'] _REQUEST_LIMITS['max_request_body_size'] = limits['maxRequestBodySize']
if 'maxOutputSize' in limits:
_REQUEST_LIMITS['max_output_size'] = limits['maxOutputSize']
def _is_blocked_ip(ip_str): def _is_blocked_ip(ip_str):
...@@ -326,8 +329,23 @@ _builtins_proxy = None # init 后创建 ...@@ -326,8 +329,23 @@ _builtins_proxy = None # init 后创建
_import_guard = False _import_guard = False
def _safe_import(name, *args, **kwargs):
def _is_direct_user_import_call():
"""判断 import 是否由用户代码直接触发,避免误伤标准库/三方包内部依赖。"""
global _import_guard global _import_guard
_import_guard = True
try:
stack = _tb.extract_stack()
finally:
_import_guard = False
if len(stack) < 3:
return False
caller_fn = stack[-3].filename or ''
return caller_fn in ('<string>', '<test>', '<module>')
def _safe_import(name, *args, **kwargs):
# 重入保护:避免 extract_stack / _original_import 内部触发 import 时无限递归 # 重入保护:避免 extract_stack / _original_import 内部触发 import 时无限递归
if _import_guard: if _import_guard:
return _original_import(name, *args, **kwargs) return _original_import(name, *args, **kwargs)
...@@ -342,16 +360,9 @@ def _safe_import(name, *args, **kwargs): ...@@ -342,16 +360,9 @@ def _safe_import(name, *args, **kwargs):
if top_level in _allowed_modules: if top_level in _allowed_modules:
return _original_import(name, *args, **kwargs) return _original_import(name, *args, **kwargs)
# 不在白名单中的模块(含危险 stdlib):检查是否由用户代码直接触发 # 不在白名单中的模块(含危险 stdlib):检查是否由用户代码直接触发
_import_guard = True
try:
stack = _tb.extract_stack()
finally:
_import_guard = False
# 只拦截直接调用者是用户代码的情况(<string>/<test>/<module>) # 只拦截直接调用者是用户代码的情况(<string>/<test>/<module>)
# stdlib 内部的间接 import(如 locale -> os)放行 # stdlib 内部的间接 import(如 locale -> os)放行
if len(stack) >= 2: if _is_direct_user_import_call():
caller_fn = stack[-2].filename or ''
if caller_fn in ('<string>', '<test>', '<module>'):
raise ImportError(f"Module '{name}' is not in the allowlist.") raise ImportError(f"Module '{name}' is not in the allowlist.")
return _original_import(name, *args, **kwargs) return _original_import(name, *args, **kwargs)
...@@ -425,7 +436,23 @@ def _safe_print(*args, **kwargs): ...@@ -425,7 +436,23 @@ def _safe_print(*args, **kwargs):
# ===== 输出 ===== # ===== 输出 =====
def write_line(obj): def write_line(obj):
sys.stdout.write(json.dumps(obj, ensure_ascii=False, default=str) + '\n') try:
payload = json.dumps(obj, ensure_ascii=False, default=str)
except Exception as e:
payload = json.dumps({
"success": False,
"message": f"Failed to serialize output: {e}",
"workerRecycle": "output_serialize"
}, ensure_ascii=False)
if len(payload.encode('utf-8')) > _REQUEST_LIMITS['max_output_size']:
payload = json.dumps({
"success": False,
"message": f"Output too large (limit: {_REQUEST_LIMITS['max_output_size']} bytes)",
"workerRecycle": "output_limit"
}, ensure_ascii=False)
sys.stdout.write(payload + '\n')
sys.stdout.flush() sys.stdout.flush()
...@@ -486,7 +513,7 @@ def _restore_modules(snapshots): ...@@ -486,7 +513,7 @@ def _restore_modules(snapshots):
# ===== 主循环 ===== # ===== 主循环 =====
def main_loop(): def main_loop():
global _allowed_modules, _request_count, _logs global _allowed_modules, _request_count, _logs, _log_size, _timeout_stage
initialized = False initialized = False
...@@ -628,7 +655,12 @@ def main_loop(): ...@@ -628,7 +655,12 @@ def main_loop():
except (Exception, SystemExit) as e: except (Exception, SystemExit) as e:
signal.alarm(0) signal.alarm(0)
write_line({"success": False, "message": str(e)}) result = {"success": False, "message": str(e)}
if isinstance(e, TimeoutError) or (
isinstance(e, SystemExit) and 'timed out' in str(e).lower()
):
result["workerRecycle"] = "timeout"
write_line(result)
finally: finally:
signal.alarm(0) signal.alarm(0)
......
...@@ -435,6 +435,7 @@ const REQUEST_LIMITS = { ...@@ -435,6 +435,7 @@ const REQUEST_LIMITS = {
timeoutMs: 60000, timeoutMs: 60000,
maxResponseSize: 10 * 1024 * 1024, maxResponseSize: 10 * 1024 * 1024,
maxRequestBodySize: 5 * 1024 * 1024, maxRequestBodySize: 5 * 1024 * 1024,
maxOutputSize: 10 * 1024 * 1024,
allowedProtocols: ['http:', 'https:'] allowedProtocols: ['http:', 'https:']
}; };
...@@ -454,10 +455,6 @@ function createHmac(algorithm: string, secret: string) { ...@@ -454,10 +455,6 @@ function createHmac(algorithm: string, secret: string) {
hmac.update(stringToSign, 'utf8'); hmac.update(stringToSign, 'utf8');
return { timestamp, sign: encodeURIComponent(hmac.digest('base64')) }; return { timestamp, sign: encodeURIComponent(hmac.digest('base64')) };
} }
function delay(ms: number): Promise<void> {
if (ms > 10000) throw new Error('Delay must be <= 10000ms');
return new Promise((r) => _workerSetTimeout(r, ms));
}
// ===== SystemHelper ===== // ===== SystemHelper =====
const SystemHelper = { const SystemHelper = {
...@@ -608,7 +605,26 @@ _ObjectFreeze(safeRequire); ...@@ -608,7 +605,26 @@ _ObjectFreeze(safeRequire);
// ===== 输出辅助 ===== // ===== 输出辅助 =====
function writeLine(obj: any): void { function writeLine(obj: any): void {
_workerStdout.write(_JSONStringify(obj) + '\n'); let line: string;
try {
line = _JSONStringify(obj);
} catch (err: any) {
line = _JSONStringify({
success: false,
message: `Failed to serialize output: ${err?.message ?? String(err)}`,
workerRecycle: 'output_serialize'
});
}
if (Buffer.byteLength(line, 'utf8') > REQUEST_LIMITS.maxOutputSize) {
line = _JSONStringify({
success: false,
message: `Output too large (limit: ${REQUEST_LIMITS.maxOutputSize} bytes)`,
workerRecycle: 'output_limit'
});
}
_workerStdout.write(line + '\n');
} }
// ===== 主循环 ===== // ===== 主循环 =====
...@@ -638,6 +654,8 @@ rl.on('line', async (line: string) => { ...@@ -638,6 +654,8 @@ rl.on('line', async (line: string) => {
REQUEST_LIMITS.maxResponseSize = msg.requestLimits.maxResponseSize; REQUEST_LIMITS.maxResponseSize = msg.requestLimits.maxResponseSize;
if (msg.requestLimits.maxRequestBodySize != null) if (msg.requestLimits.maxRequestBodySize != null)
REQUEST_LIMITS.maxRequestBodySize = msg.requestLimits.maxRequestBodySize; REQUEST_LIMITS.maxRequestBodySize = msg.requestLimits.maxRequestBodySize;
if (msg.requestLimits.maxOutputSize != null)
REQUEST_LIMITS.maxOutputSize = msg.requestLimits.maxOutputSize;
} }
hardenRuntime(); hardenRuntime();
writeLine({ type: 'ready' }); writeLine({ type: 'ready' });
...@@ -731,8 +749,15 @@ rl.on('line', async (line: string) => { ...@@ -731,8 +749,15 @@ rl.on('line', async (line: string) => {
} }
activeIntervals.clear(); activeIntervals.clear();
}; };
const safeDelay = (ms: number): Promise<void> => {
if (ms > 10000) throw new Error('Delay must be <= 10000ms');
return new _OriginalPromise((resolve) => {
safeSetTimeout(resolve, ms);
});
};
let timer: ReturnType<typeof setTimeout> | undefined; let timer: ReturnType<typeof setTimeout> | undefined;
let timedOut = false;
const requireCacheKeysBeforeTask = getRequireCacheKeys(); const requireCacheKeysBeforeTask = getRequireCacheKeys();
try { try {
assertNoDynamicImport(code); assertNoDynamicImport(code);
...@@ -808,7 +833,7 @@ rl.on('line', async (line: string) => { ...@@ -808,7 +833,7 @@ rl.on('line', async (line: string) => {
countToken, countToken,
strToBase64, strToBase64,
createHmac, createHmac,
delay, safeDelay,
httpRequest, httpRequest,
variables || {}, variables || {},
undefined, undefined,
...@@ -861,7 +886,10 @@ rl.on('line', async (line: string) => { ...@@ -861,7 +886,10 @@ rl.on('line', async (line: string) => {
const timeoutPromise = new _OriginalPromise((_, reject) => { const timeoutPromise = new _OriginalPromise((_, reject) => {
timer = _workerSetTimeout( timer = _workerSetTimeout(
() => reject(new _OriginalError(`Script execution timed out after ${timeoutMs}ms`)), () => {
timedOut = true;
reject(new _OriginalError(`Script execution timed out after ${timeoutMs}ms`));
},
timeoutMs || 10000 timeoutMs || 10000
); );
}); });
...@@ -874,7 +902,11 @@ rl.on('line', async (line: string) => { ...@@ -874,7 +902,11 @@ rl.on('line', async (line: string) => {
}); });
} catch (err: any) { } catch (err: any) {
_workerClearTimeout(timer); _workerClearTimeout(timer);
writeLine({ success: false, message: err?.message ?? String(err) }); writeLine({
success: false,
message: err?.message ?? String(err),
...(timedOut ? { workerRecycle: 'timeout' } : {})
});
} finally { } finally {
cleanupUserTimers(); cleanupUserTimers();
cleanupUserRequireCache(requireCacheKeysBeforeTask); cleanupUserRequireCache(requireCacheKeysBeforeTask);
......
import { join } from 'path';
import { BaseProcessPool } from '../../src/pool/base-process-pool';
export type SandboxLanguage = 'JS' | 'Python';
export class CustomModuleProcessPool extends BaseProcessPool {
constructor(
language: SandboxLanguage,
allowedModules: readonly string[],
options: { recycleAfterTask?: boolean } = {}
) {
super(1, {
name: `Custom${language}`,
workerScript: join(process.cwd(), 'src/pool', language === 'JS' ? 'worker.ts' : 'worker.py'),
spawnCommand: (script) =>
language === 'JS' ? `exec tsx ${script}` : `exec python3 -u ${script}`,
allowedModules,
recycleAfterTask: options.recycleAfterTask
});
}
}
export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export async function waitForCondition(
predicate: () => boolean,
timeoutMs = 5000
): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return true;
await wait(100);
}
return predicate();
}
export async function waitForPidExit(pid: number, timeoutMs = 5000): Promise<boolean> {
return waitForCondition(() => {
try {
process.kill(pid, 0);
return false;
} catch {
return true;
}
}, timeoutMs);
}
export function getWorkerPid(pool: BaseProcessPool): number | undefined {
return (pool as unknown as { workers?: Array<{ proc?: { pid?: number } }> }).workers?.[0]?.proc
?.pid;
}
...@@ -287,10 +287,25 @@ describe('API 错误处理安全', () => { ...@@ -287,10 +287,25 @@ describe('API 错误处理安全', () => {
} else { } else {
// catch 分支 // catch 分支
expect(data.success).toBe(false); expect(data.success).toBe(false);
console.log(data, 123213213);
expect(data.message).toContain('is not valid JSON'); expect(data.message).toContain('is not valid JSON');
} }
}); });
it('超大 JSON body 在进入执行前返回 413', async () => {
const res = await app.request('/sandbox/js', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({
code: 'async function main() { return { ok: true } }',
variables: { text: 'x'.repeat(1024 * 1024 + 1) }
})
});
const data = await res.json();
expect(res.status).toBe(413);
expect(data.success).toBe(false);
expect(data.message).toMatch(/body too large/i);
});
}); });
// ===== Zod 校验失败(有效 JSON 但 schema 不匹配) ===== // ===== Zod 校验失败(有效 JSON 但 schema 不匹配) =====
......
...@@ -11,7 +11,14 @@ ...@@ -11,7 +11,14 @@
import { describe, it, expect, afterEach, beforeAll } from 'vitest'; import { describe, it, expect, afterEach, beforeAll } from 'vitest';
import { ProcessPool } from '../../src/pool/process-pool'; import { ProcessPool } from '../../src/pool/process-pool';
import { PythonProcessPool } from '../../src/pool/python-process-pool'; import { PythonProcessPool } from '../../src/pool/python-process-pool';
import { BaseProcessPool } from '../../src/pool/base-process-pool';
import { env, RUNTIME_MEMORY_OVERHEAD_MB } from '../../src/env'; import { env, RUNTIME_MEMORY_OVERHEAD_MB } from '../../src/env';
import {
CustomModuleProcessPool,
getWorkerPid,
waitForCondition,
waitForPidExit
} from '../helpers/custom-process-pool';
beforeAll(async () => { beforeAll(async () => {
console.log(`\n=== Memory Limit Test Status ===`); console.log(`\n=== Memory Limit Test Status ===`);
...@@ -40,9 +47,6 @@ describe('内存限制', () => { ...@@ -40,9 +47,6 @@ describe('内存限制', () => {
await pool.init(); await pool.init();
expect(pool.stats.total).toBe(1); expect(pool.stats.total).toBe(1);
// 实际限制 = 用户配置 + 运行时开销(50MB)
const actualLimitMB = env.SANDBOX_MAX_MEMORY_MB + RUNTIME_MEMORY_OVERHEAD_MB;
const result = await pool.execute({ const result = await pool.execute({
code: `async function main() { code: `async function main() {
const arr = []; const arr = [];
...@@ -144,6 +148,85 @@ describe('Python 内存限制', () => { ...@@ -144,6 +148,85 @@ describe('Python 内存限制', () => {
}, 30000); }, 30000);
}); });
describe('显式放开后台执行模块时的 worker 回收', () => {
let pool: BaseProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('JS 显式允许 child_process 后,任务返回即回收 worker 并清理子进程', async () => {
pool = new CustomModuleProcessPool('JS', ['child_process'], { recycleAfterTask: true });
await pool.init();
const firstPid = getWorkerPid(pool);
expect(firstPid).toBeTypeOf('number');
const result = await pool.execute({
code: `async function main() {
const { spawn } = require('child_process');
const child = spawn('node', ['-e', 'setInterval(() => {}, 1000)'], {
stdio: 'ignore'
});
child.unref();
return { childPid: child.pid };
}`,
variables: {}
});
expect(result.success).toBe(true);
const childPid = result.data?.codeReturn.childPid;
expect(childPid).toBeTypeOf('number');
const workerRecycled = await waitForCondition(() => {
const currentPid = getWorkerPid(pool);
return typeof currentPid === 'number' && currentPid !== firstPid;
});
expect(workerRecycled).toBe(true);
expect(await waitForPidExit(childPid)).toBe(true);
const recovery = await pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
expect(recovery.success).toBe(true);
expect(recovery.data?.codeReturn.ok).toBe(true);
}, 20000);
it('Python 显式允许 subprocess 后,任务返回即回收 worker 并清理子进程', async () => {
pool = new CustomModuleProcessPool('Python', ['subprocess'], { recycleAfterTask: true });
await pool.init();
const firstPid = getWorkerPid(pool);
expect(firstPid).toBeTypeOf('number');
const result = await pool.execute({
code: `import subprocess\ndef main():\n child = subprocess.Popen(['python3', '-c', 'import time; time.sleep(60)'])\n return {'childPid': child.pid}`,
variables: {}
});
expect(result.success).toBe(true);
const childPid = result.data?.codeReturn.childPid;
expect(childPid).toBeTypeOf('number');
const workerRecycled = await waitForCondition(() => {
const currentPid = getWorkerPid(pool);
return typeof currentPid === 'number' && currentPid !== firstPid;
});
expect(workerRecycled).toBe(true);
expect(await waitForPidExit(childPid)).toBe(true);
const recovery = await pool.execute({
code: `def main():\n return {'ok': True}`,
variables: {}
});
expect(recovery.success).toBe(true);
expect(recovery.data?.codeReturn.ok).toBe(true);
}, 20000);
});
// ============================================================ // ============================================================
// 2. CPU 限制 // 2. CPU 限制
// ============================================================ // ============================================================
...@@ -285,6 +368,26 @@ describe('JS 运行时长限制', () => { ...@@ -285,6 +368,26 @@ describe('JS 运行时长限制', () => {
expect(elapsed).toBeLessThan(env.SANDBOX_MAX_TIMEOUT + 10000); expect(elapsed).toBeLessThan(env.SANDBOX_MAX_TIMEOUT + 10000);
}); });
it('异步超时后回收 worker,避免超时任务残留到下一次执行', async () => {
pool = new ProcessPool(1);
await pool.init();
const pidBefore = (pool as any).workers[0].proc.pid;
const result = await pool.execute({
code: `async function main() {
await delay(${env.SANDBOX_MAX_TIMEOUT + 5000});
return { done: true };
}`,
variables: {}
});
await new Promise((r) => setTimeout(r, 1500));
const pidAfter = (pool as any).workers[0].proc.pid;
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
expect(pidAfter).not.toBe(pidBefore);
});
it('在超时范围内完成的代码正常返回', async () => { it('在超时范围内完成的代码正常返回', async () => {
pool = new ProcessPool(1); pool = new ProcessPool(1);
await pool.init(); await pool.init();
...@@ -341,6 +444,23 @@ describe('Python 运行时长限制', () => { ...@@ -341,6 +444,23 @@ describe('Python 运行时长限制', () => {
expect(elapsed).toBeLessThan(env.SANDBOX_MAX_TIMEOUT + 10000); expect(elapsed).toBeLessThan(env.SANDBOX_MAX_TIMEOUT + 10000);
}); });
it('超时后回收 Python worker,避免信号和模块状态残留', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const pidBefore = (pool as any).workers[0].proc.pid;
const result = await pool.execute({
code: `import time\ndef main():\n time.sleep(${Math.ceil(env.SANDBOX_MAX_TIMEOUT / 1000) + 5})\n return {'done': True}`,
variables: {}
});
await new Promise((r) => setTimeout(r, 1500));
const pidAfter = (pool as any).workers[0].proc.pid;
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
expect(pidAfter).not.toBe(pidBefore);
});
it('在超时范围内完成的代码正常返回', async () => { it('在超时范围内完成的代码正常返回', async () => {
pool = new PythonProcessPool(1); pool = new PythonProcessPool(1);
await pool.init(); await pool.init();
...@@ -366,7 +486,86 @@ describe('Python 运行时长限制', () => { ...@@ -366,7 +486,86 @@ describe('Python 运行时长限制', () => {
}); });
// ============================================================ // ============================================================
// 4. 网络请求次数限制 // 4. 输出大小限制
// ============================================================
describe('JS 输出大小限制', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('返回值超过 maxOutputSize 被拒绝且 worker 可恢复', async () => {
pool = new ProcessPool(1);
await pool.init();
const pidBefore = getWorkerPid(pool);
const result = await pool.execute({
code: `async function main() {
return 'x'.repeat(${env.SANDBOX_MAX_OUTPUT_MB} * 1024 * 1024 + 1);
}`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toMatch(/output too large/i);
const workerRecycled = await waitForCondition(() => {
const currentPid = getWorkerPid(pool);
return typeof currentPid === 'number' && currentPid !== pidBefore;
});
expect(workerRecycled).toBe(true);
const recovery = await pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
expect(recovery.success).toBe(true);
expect(recovery.data?.codeReturn.ok).toBe(true);
}, 20000);
});
describe('Python 输出大小限制', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('返回值超过 maxOutputSize 被拒绝且 worker 可恢复', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const pidBefore = getWorkerPid(pool);
const result = await pool.execute({
code: `def main():\n return 'x' * (${env.SANDBOX_MAX_OUTPUT_MB} * 1024 * 1024 + 1)`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toMatch(/output too large/i);
const workerRecycled = await waitForCondition(() => {
const currentPid = getWorkerPid(pool);
return typeof currentPid === 'number' && currentPid !== pidBefore;
});
expect(workerRecycled).toBe(true);
const recovery = await pool.execute({
code: `def main():\n return {'ok': True}`,
variables: {}
});
expect(recovery.success).toBe(true);
expect(recovery.data?.codeReturn.ok).toBe(true);
}, 20000);
});
// ============================================================
// 5. 网络请求次数限制
// ============================================================ // ============================================================
describe('JS 网络请求次数限制', () => { describe('JS 网络请求次数限制', () => {
let pool: ProcessPool; let pool: ProcessPool;
...@@ -482,7 +681,7 @@ describe('Python 网络请求次数限制', () => { ...@@ -482,7 +681,7 @@ describe('Python 网络请求次数限制', () => {
}); });
// ============================================================ // ============================================================
// 5. 网络请求大小限制 // 6. 网络请求大小限制
// ============================================================ // ============================================================
describe('JS 请求体大小限制', () => { describe('JS 请求体大小限制', () => {
let pool: ProcessPool; let pool: ProcessPool;
...@@ -575,7 +774,7 @@ describe('Python 请求体大小限制', () => { ...@@ -575,7 +774,7 @@ describe('Python 请求体大小限制', () => {
}); });
// ============================================================ // ============================================================
// 6. 网络协议限制 // 7. 网络协议限制
// ============================================================ // ============================================================
describe('JS 网络协议限制', () => { describe('JS 网络协议限制', () => {
let pool: ProcessPool; let pool: ProcessPool;
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
import { afterEach, describe, it, expect, beforeAll, afterAll } from 'vitest'; import { afterEach, describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ProcessPool } from '../../src/pool/process-pool'; import { ProcessPool } from '../../src/pool/process-pool';
import { PythonProcessPool } from '../../src/pool/python-process-pool'; import { PythonProcessPool } from '../../src/pool/python-process-pool';
import { CustomModuleProcessPool } from '../helpers/custom-process-pool';
let jsPool: ProcessPool; let jsPool: ProcessPool;
let pyPool: PythonProcessPool; let pyPool: PythonProcessPool;
...@@ -65,6 +66,35 @@ describe('模块拦截', () => { ...@@ -65,6 +66,35 @@ describe('模块拦截', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
it('显式加入白名单后允许 Node 内置模块', async () => {
const pool = new CustomModuleProcessPool('JS', [
'fs',
'node:fs',
'fs/promises',
'child_process',
'http',
'lodash'
]);
await pool.init();
try {
const payloads = [
`async function main() { const fs = require('fs'); return { ok: typeof fs.readFileSync === 'function' }; }`,
`async function main() { const fs = require('node:fs'); return { ok: typeof fs.readFileSync === 'function' }; }`,
`async function main() { const fs = require('fs/promises'); return { ok: typeof fs.readFile === 'function' }; }`,
`async function main() { const cp = require('child_process'); return { ok: typeof cp.execFile === 'function' }; }`,
`async function main() { const http = require('http'); return { ok: typeof http.request === 'function' }; }`
];
for (const code of payloads) {
const result = await pool.execute({ code, variables: {} });
expect(result.success).toBe(true);
expect(result.data?.codeReturn.ok).toBe(true);
}
} finally {
await pool.shutdown();
}
});
it('阻止 require https', async () => { it('阻止 require https', async () => {
const result = await runner.execute({ const result = await runner.execute({
code: `async function main() { const https = require('https'); return {}; }`, code: `async function main() { const https = require('https'); return {}; }`,
...@@ -248,6 +278,33 @@ describe('模块拦截', () => { ...@@ -248,6 +278,33 @@ describe('模块拦截', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
it('显式加入白名单后允许危险 Python 标准库', async () => {
const pool = new CustomModuleProcessPool('Python', [
'os',
'subprocess',
'socket',
'pathlib',
'json'
]);
await pool.init();
try {
const payloads = [
`import os\ndef main():\n return {'ok': hasattr(os, 'getcwd')}`,
`import subprocess\ndef main():\n return {'ok': hasattr(subprocess, 'Popen')}`,
`import socket\ndef main():\n return {'ok': hasattr(socket, 'socket')}`,
`from pathlib import Path\ndef main():\n return {'ok': Path('/tmp').name == 'tmp'}`
];
for (const code of payloads) {
const result = await pool.execute({ code, variables: {} });
expect(result.success).toBe(true);
expect(result.data?.codeReturn.ok).toBe(true);
}
} finally {
await pool.shutdown();
}
});
it('阻止 import requests(预检)', async () => { it('阻止 import requests(预检)', async () => {
const result = await runner.execute({ const result = await runner.execute({
code: `import requests\ndef main():\n return {}`, code: `import requests\ndef main():\n return {}`,
......
...@@ -17,6 +17,7 @@ export default defineConfig({ ...@@ -17,6 +17,7 @@ export default defineConfig({
isolate: false, isolate: false,
env: { env: {
CHECK_INTERNAL_IP: 'true', CHECK_INTERNAL_IP: 'true',
SANDBOX_API_MAX_BODY_MB: '1',
SANDBOX_MAX_TIMEOUT: '5000', SANDBOX_MAX_TIMEOUT: '5000',
SANDBOX_TOKEN: 'test' SANDBOX_TOKEN: 'test'
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment