L.Leverage Learn从第一行代码,到读懂真实后端查看项目源码

OJ 评测系统 / 第 21 课

沙箱隔离与安全边界

从容器创建参数理解不可信用户代码的隔离措施,以及 Docker 仍留下的风险。

◷ 预计 17 分钟 · 面向初学者

用户提交的代码是不可信输入。它可能意外死循环、耗尽内存、无限输出,也可能主动尝试读取文件、访问网络或利用运行环境。不能在 NestJS 主进程里对代码调用 exec、eval 或 shell 执行;那会把用户程序和承载 HTTP、队列、密钥的应用进程放在同一个权限边界内。

公开实现通过 Docker CLI 创建临时容器,并分别准备编译和运行。创建参数显式关闭网络(--network=none),移除 Linux capabilities,启用 no-new-privileges,设置只读根文件系统;临时可写空间限制在 /tmp 的 tmpfs 中。容器以非 root 的 65534:65534 用户启动,并配置内存/swap、CPU、PID 数、文件描述符数量等限制。日志驱动也关闭,容器完成或超时后会尝试移除。

源代码和输入采用管道及长度前缀传递:源码先写入编译容器的标准输入,产物从标准输出取回;运行容器通过启动协议接收产物、运行上下文和测试输入。该路径没有把宿主机目录 bind mount 到任务容器,也不把应用环境变量或 Docker socket 传进去。文件名、参数、文件数量和大小会在主机端验证。这些选择减少了用户代码直接触达宿主机文件和运行服务凭证的机会。测试数据目录由可信评测进程读取,不应因此理解为容器获得了宿主机测试数据路径。

HTTP / Worker 主进程(可信)
   │ 受控输入 / 产物通道
   ▼
临时任务容器(不可信代码)── network=none
   │ 有界 stdout / stderr 与退出状态
   ▼
评测引擎(比较结果并结算)

容器不是安全证明。它与宿主机仍共享内核;内核或 Docker runtime 漏洞、守护进程配置错误、权限过宽、映像供应链问题都可能破坏预期边界。Docker daemon 本身是高权限组件;如果承载应用的主机能被容器控制,风险不因代码“在 Docker 里”就消失。CPU 与内存配额也不能独立解决所有拒绝服务:容器创建量、主机磁盘、队列积压、日志和并发都会影响服务可用性。并发上限、超时、输出截断与清理流程属于整体防护的一部分。

审阅安全设计时,要看参数是否实际进入 docker create,而不是只看配置项名字;要确认没有意外挂载、继承敏感环境变量或开放网络,还要检查取消和错误路径能否清理容器。生产部署仍需要最小权限的宿主机、及时更新的内核与 Docker、可信且固定的运行镜像、资源监控和隔离的工作节点。更强威胁模型可考虑独立 VM 或专用沙箱;本课引用的公开实现是 Docker 沙箱,不应把本地未发布的优化说成它已有的能力。

隔离还要覆盖信任边界两侧:控制 Docker 的是评测服务而非提交者,镜像不能从不可信提交中构建;容器内只放运行所需程序和临时数据,不应放数据库密码、JWT 密钥或源码仓库凭证。宿主机收集输出和状态后,评测进程仍需限制日志长度、过滤不适合展示的控制字符并保护测试数据。即便主程序不挂载测试目录,调试信息也可能意外泄露输入;“文件系统隔离”和“结果信息保密”是不同目标,设计时都要说明。

小练习

若移除 --network=none,用户程序可能多出什么能力?--read-only 是否代表容器里没有任何可写位置?

**答案:**它可能尝试连接可达网络服务;关闭网络也不能替代其他隔离。只读根文件系统仍可有明确授予的可写临时空间,例如 /tmp tmpfs,因此要检查挂载和大小限制。

去真实仓库看看

固定公开提交 de34b268e75f293e904d7734c194a4ff2467b7a8:

互动练习:隔离不可信提交

评测 worker 执行用户提交的任意代码。已知该项目的执行容器使用 Docker(不是 nsjail)。哪种说法最安全且准确?

选择一个答案

✓
这一课,你已经能……

用户程序必须在受限隔离环境中运行;容器设置能降低风险,但不等于完全安全。