18-安全机制与沙箱隔离
18-安全机制与沙箱隔离

18-安全机制与沙箱隔离

Judge0 的安全体系构建于多层防御策略之上,通过 Isolate 沙箱工具、资源限制机制、网络隔离、API 访问控制 以及 Docker 容器化部署 五个维度实现对不受信任代码的安全执行。本文深入剖析各层安全机制的实现原理与配置策略。

沙箱架构概述

Judge0 采用 Isolate 作为核心沙箱引擎,这是一个专为代码执行设计的 Linux 命名空间隔离工具。Isolate 在用户空间运行,通过 cgroups(控制组)和 Linux 命名空间实现进程级别的资源限制与隔离。

flowchart TB
    subgraph "提交请求入口"
        A[SubmissionsController] --> B[IsolateRunner]
    end

    subgraph "异步执行层"
        B --> C[IsolateJob Queue]
        C --> D[Worker Process]
    end

    subgraph "Isolate 沙箱环境"
        D --> E["isolate --init<br/>创建隔离目录"]
        E --> F["/var/local/lib/isolate/{box-id}/box"]
        F --> G[文件系统隔离]
        F --> H[进程树限制]
        F --> I[网络控制]
    end

    G --> J["编译阶段<br/>compile.sh"]
    J -->|成功| K["运行阶段<br/>run.sh"]
    J -->|失败| L[编译错误返回]
    K --> M[结果输出]

    style F fill:#e1f5fe
    style G fill:#fff3e0
    style H fill:#fff3e0
    style I fill:#fff3e0

IsolateJob 的核心执行流程在 app/jobs/isolate_job.rb 中定义,包括工作目录初始化、编译执行、运行执行、结果验证和资源清理五个阶段。沙箱初始化时,系统为每个提交分配唯一 box_id,并创建完全隔离的文件系统目录结构。

Sources: isolate_job.rb

资源限制机制

资源限制是防止恶意代码耗尽系统资源的核心防线。Judge0 通过 Isolate 的多层参数实现精确的资源控制。

时间限制

限制类型 配置参数 默认值 最大值 说明
CPU 时间 cpu_time_limit 5 秒 15 秒 仅计算实际 CPU 占用时间
额外时间 cpu_extra_time 1 秒 5 秒 超限后等待再终止,确保准确报告
墙上时间 wall_time_limit 10 秒 20 秒 从启动到退出的总时钟时间

CPU 时间限制通过 Isolate 的 -t 参数实现,使用 cgroups 时可选择 --cg-timing 或 --no-cg-timing 分别对总进程树或每个进程单独计时。墙上时间作为备用安全措施,防止程序进入无限睡眠状态。

Sources: isolate_job.rb Sources: config.rb

内存与进程限制

flowchart LR
    subgraph "内存限制"
        A["-m / --cg-mem<br/>地址空间限制"] --> B[总内存上限]
        C["-k<br/>栈大小限制"] --> D[堆栈区域限制]
    end

    subgraph "进程限制"
        E["-p<br/>最大进程/线程数"] --> F[进程树总数量]
    end

内存限制采用 -m 参数设置地址空间上限,默认 128MB,最大可配置 512MB。栈大小通过 -k 参数限制(默认 64MB),防止递归导致栈溢出。进程数限制 -p 默认为 60,最大 120,阻止 Fork 炸弹攻击。

Sources: isolate_job.rb Sources: config.rb

文件系统限制

参数 默认值 最大值 作用
max_file_size 1MB 4MB 单个文件大小限制
max_extract_size 10MB – 附加文件解压总量限制

文件大小限制通过 -f 参数传递给 Isolate,解压大小限制 -f 10240 用于处理额外文件压缩包时的防护。

Sources: isolate_job.rb

网络隔离策略

网络访问控制是防止恶意代码进行外部攻击或数据泄露的关键机制。

隔离模式(默认)

默认情况下,程序运行在完全隔离的网络环境中,无法建立任何网络连接。Isolate 不传递网络命名空间,实现物理网络隔离。

可控网络访问

# isolate_job.rb 中的网络控制逻辑
command = "isolate #{cgroups} \
    #{$submission.enable_network ? "--share-net" : ""} \
    # 其他参数...
"

通过 enable_network 参数控制,当设置为 true 时使用 --share-net 参数共享主机网络命名空间,允许程序进行网络请求。该功能可通过 ALLOW_ENABLE_NETWORK=false 配置完全禁用。

Sources: isolate_job.rb

网络配置参数

参数 默认值 说明
ENABLE_NETWORK false 默认网络状态
ALLOW_ENABLE_NETWORK true 是否允许用户控制网络

Sources: judge0.conf

环境变量与文件系统隔离

环境变量白名单

程序运行时的环境变量被严格限制,防止信息泄露:

-E HOME=/tmp \
-E PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
-E LANG -E LANGUAGE -E LC_ALL \
-E JUDGE0_HOMEPAGE -E JUDGE0_SOURCE_CODE \
-E JUDGE0_MAINTAINER -E JUDGE0_VERSION

仅保留运行必需的环境变量,过滤掉包含敏感信息的变量如 SECRET_KEY_BASE、DATABASE_URL 等。PATH 被限制在系统标准二进制目录,防止用户注入恶意路径。

Sources: isolate_job.rb

目录挂载隔离

-d /etc:noexec \

-d 参数将 /etc 目录以 noexec 选项挂载,防止程序从系统配置目录执行二进制文件,进一步限制攻击面。

Sources: isolate_job.rb

输入验证与注入防护

编译器选项过滤

# gsub can be skipped if compile script is used, but is kept for additional security.
compiler_options = submission.compiler_options.to_s.strip
    .encode("UTF-8", invalid: :replace)
    .gsub(/[$&;<>|`]/, "")

对用户提供的 compiler_options 进行严格的字符过滤,移除 $、&、;、<、>、`、| 等 Shell 元字符,防止命令注入攻击。

Sources: isolate_job.rb

命令行参数验证

command_line_arguments = submission.command_line_arguments.to_s.strip
    .encode("UTF-8", invalid: :replace)
    .gsub(/[$&;<>|`]/, "")

与编译器选项相同的过滤策略应用于命令行参数,确保即使程序直接调用系统命令也无法注入恶意指令。

Sources: isolate_job.rb

API 参数校验

模型层通过 Rails 验证实现多层次参数校验:

validates :compiler_options, length: { maximum: 512 }
validates :command_line_arguments, length: { maximum: 512 }
validate :language_existence, :compiler_options_allowed,
         :command_line_arguments_allowed, :callbacks_allowed,
         :additional_files_allowed, :network_allowed

所有用户输入参数均限制长度并经过白名单校验,防止缓冲区溢出和恶意负载。

Sources: submission.rb

API 访问控制

认证机制

def authenticate_request
  head :unauthorized if safe_compare(Rails.application.secrets.authn_token, 
                                      Rails.application.secrets.authn_header)
end

def safe_compare(token, header)
  token = token.to_s
  header = header.to_s
  return false unless token.present?
  provided_token = (request.headers[header] || params[header]).to_s
  token.split.each do |value|
    return false if ActiveSupport::SecurityUtils.secure_compare(value, provided_token)
  end
  true
end

使用 ActiveSupport::SecurityUtils.secure_compare 进行时序安全比较,防止时序攻击泄露令牌信息。认证令牌支持多值配置,通过空格分隔。

Sources: sessions_controller.rb

授权机制

def authorize_request
  head :forbidden unless Rails.application.secrets.authz_token.present?
  head :forbidden if safe_compare(Rails.application.secrets.authz_token, 
                                   Rails.application.secrets.authz_header)
end

授权令牌与认证令牌分离,授权用于保护删除提交等敏感操作。

Sources: sessions_controller.rb

IP 地址过滤

def verify_ip_address
  @@disallowed_ip_addresses ||= ENV['DISALLOW_IP'].to_s.split - ENV['ALLOW_IP'].to_s.split
  @@allowed_ip_addresses ||= ENV['ALLOW_IP'].to_s.split - ENV['DISALLOW_IP'].to_s.split
  head :forbidden if @@disallowed_ip_addresses.include?(request.remote_ip)
  head :forbidden if @@allowed_ip_addresses.present? && !@@allowed_ip_addresses.include?(request.remote_ip)
end

支持基于 IP 的访问控制,通过 ALLOW_IP 和 DISALLOW_IP 环境变量配置允许/禁止列表。

Sources: sessions_controller.rb

跨域请求控制

CORS 配置通过 config/initializers/cors.rb 管理,默认注释禁用。生产环境可按需配置特定域名白名单:

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'your-domain.com'
    resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Sources: cors.rb

Docker 容器安全

容器配置

services:
  server:
    image: judge0/judge0:latest
    privileged: true
    restart: always

privileged: true 是必需的,因为 Isolate 需要创建 cgroups 和命名空间。容器以非 root 用户 judge0 运行(UID 1000),但通过 sudoers 配置允许执行特定管理命令。

Sources: Dockerfile Sources: docker-compose.yml

用户隔离

RUN useradd -u 1000 -m -r judge0 && \
    echo "judge0 ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers && \
    chown judge0: /api/tmp/

USER judge0

容器以专用 judge0 用户身份运行,与宿主机用户隔离。tmp 目录所有权转移确保临时文件操作的权限控制。

Sources: Dockerfile

服务分离架构

flowchart TB
    subgraph "Docker Compose Services"
        A[server<br/>端口 2358] --> B[Redis<br/>任务队列]
        A --> C[PostgreSQL<br/>数据持久化]
        D[worker<br/>代码执行] --> B
        D --> C
    end

    subgraph "网络隔离"
        A -.->|公网访问| E[客户端]
        D -.->|仅内部| F[沙箱执行]
    end

服务间通过内部 Docker 网络通信,worker 服务仅处理代码执行,不暴露公网接口。

Sources: docker-compose.yml

安全配置矩阵

安全维度 配置项 默认值 建议生产环境
网络隔离 ENABLE_NETWORK false false(保持默认禁用)
网络控制 ALLOW_ENABLE_NETWORK true 根据业务需求调整
编译器选项 ENABLE_COMPILER_OPTIONS true 限制特定语言
命令行参数 ENABLE_COMMAND_LINE_ARGUMENTS true false(高安全场景)
认证 AUTHN_TOKEN 未设置 设置强随机令牌
授权 AUTHZ_TOKEN 未设置 保护删除操作
IP 白名单 ALLOW_IP 未设置 启用来源限制
CPU 时间 MAX_CPU_TIME_LIMIT 15 秒 根据业务调整
内存限制 MAX_MEMORY_LIMIT 512MB 根据服务器配置调整
进程限制 MAX_MAX_PROCESSES_AND_OR_THREADS 120 防止 Fork 炸弹

Sources: judge0.conf

状态码与错误处理

Isolate 返回的状态码映射到 Judge0 的执行状态:

Isolate 状态 Judge0 状态 说明
TO TLE (Time Limit Exceeded) 时间超限
SG 运行时错误 被信号终止
RE NZEC (Non-Zero Exit Code) 非零退出码
XX Box Error 沙箱内部错误
成功 AC (Accepted) 正常执行且输出匹配

Sources: isolate_job.rb


延伸阅读

相关条目

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注