---
title: "Kobako：為什麼沙盒裡刻意沒有時間？"
date: 2026-11-11T00:00:00+08:00
publishDate: 2026-11-11T00:00:00+08:00
lastmod: 2026-11-09T21:49:14+08:00
tags: ["LLM","AI","經驗","Ruby","WebAssembly","Gem","mruby","Rust"]
series: "kobako"
toc: true
cover: "https://blog.aotoki.me/posts/2026/11/11/kobako-why-sandbox-has-no-time/cover_hu_a5c31ed7d797e3e9.webp"
permalink: "https://blog.aotoki.me/posts/2026/11/11/kobako-why-sandbox-has-no-time/"
language: "zh-tw"
---

當我們把 RPC、Handle、[Block](https://blog.aotoki.me/posts/2026/11/04/kobako-block-through-sandbox/) 等機制完成後，已經是一個很好用的 mruby 腳本環境，但是作為沙盒（Sandbox）最關鍵的能力就是「安全性」也就是執行任意腳本時，都當作有危險去看待，那麼就需要有各種防禦情境。

## 旁路攻擊

[旁路攻擊（Side-Channel Attack）](https://zh.wikipedia.org/zh-tw/%E6%97%81%E8%B7%AF%E6%94%BB%E5%87%BB)屬於相當有名的攻擊手段，尤其像 Kobako 這類專案可以執行完整的 Ruby 語法時，就更容易有機會被攻擊，以下面作為例子。

```ruby
sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))

# ...
sandbox.eval(<<~SANDBOX)
  start = Time.now
  UserRepository.find(email: "...")
  Time.now - start
SANDBOX

# => 10ms
# => 1ms
# => 1ms
# => 12ms
```

隨機選擇四個 Email 依序查詢時，發現兩種時間範圍 `10ms` 左右和 `1ms` 左右兩種，此時可以猜到有一半的 Email 可能是真實存在的，因為資料庫查詢命中跟非命中有時間差，或者在透過 Kobako 的繫結機制時，因為解析需要比較多時間。

不管理由是怎樣，只要嘗試足夠多，或者使用網路上已經外洩的信箱嘗試，那麼很快的就可以知道這個系統內部是否存在這些使用者，就有機會更進一步做其他嘗試。

> 雖然前提是沙盒被利用，但是一個專案少一個攻擊點，整體就會更安全，因此在設計時就需要考量要提供怎樣的機制。

## 凍結時間

Kobako 預設提供的設定叫做 `hermetic`（密閉）模式，而且不引入 `mruby-time` 這個原生的擴充，所有跟時間相關的機制都不存在於沙盒內。

簡單來說，如果想問時間，因為沒有 `Time` 所以問不到。即使想要用其他方式測量，對沙盒來說只會是「經過多少單位」而不是準確的多少秒、毫秒，那麼想要利用旁路攻擊的機會就會變得非常低。

然而，很多情境我們還是需要有「時間」的存在，在大多數情境中，我們並不需要精確度非常高的時間，至少不會有人問像是「幾點幾分幾秒註冊？」這樣的問題，想要篩選使用者頂多精確到幾日就足夠，那麼可以很簡單的利用原本的繫結（Bind）能力，提供一個低精確度的時間。

```ruby
sandbox.bind("Date::Today", -> { Date.today.to_s })
```

這樣想要做到「某個使用者是否存在」這樣的測量，就會因為「一天」的顆粒太粗，不管怎樣嘗試都無法確認，一定程度上就可以避開。

當然，大多數情境並不需要這麼極端的設計，通常只需要提供到秒為單位，就很難大量利用跟嘗試，至少在那之前會先因為沙盒限制或者限速之類的機制被阻擋下來。

```ruby
sandbox.bind("CurrentTime", -> { Time.now.to_i })
```

基本上只要提供像這樣的設計，就非常足夠大多數情境使用。

## 安全取捨

這個完全封閉的設計在初期 Kobako 的情境下基本上是安全的，但是為了對應 AI 的使用情境，最近開始支援[虛擬檔案系統（Virtual Filesystem）](https://blog.aotoki.me/posts/2026/10/21/kobako-native-filesystem-support/)以及網路（Networking）的狀況下，又把這些風險帶回到專案。

```ruby
sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))

# ...
sandbox.eval(<<~SANDBOX)
  File.write('/mnt/.timestamp', '')
  start = File.mtime('/mnt/.timestamp')
  UserRepository.find(email: "...")
  File.write('/mnt/.timestamp', '')
  File.mtime('/mnt/.timestamp') - start
SANDBOX
```

當我們可以存取檔案時，馬上又變成新的破口，檔案的修改時間會被呈現出來，最終還是無法避免時間被推論出來。

也因此在設計虛擬檔案系統時，如果是在 `hermetic` 模式的狀況下，Kobako 會把 `mtime`（修改時間）固定在 1970 年的起點（Epoch），這樣就可以封鎖掉這一條路徑。

不過，還是有很多可能的機會，像是這樣的方式

```ruby
sandbox.eval(<<~SANDBOX)
  start = Net::HTTP.get('https://hacker.com/timestamp').to_i
  UserRepository.find(email: "...")
  Net::HTTP.get('https://hacker.com/timestamp').to_i - start
SANDBOX
```

假設有一台伺服器可以固定回傳時間，只需要讓沙盒中的程式發起請求就好，不過這點難度實際上困難不少，因為網路傳輸如果不是很剛好在非常接近的位置，那麼就沒辦法在夠短的時間內回傳，很可能還是測量不到。

另一種可能的路徑是針對宿主環境的查詢，這樣延遲大多會落在 `1ms` 內就有機會被利用，因此大多數沙盒對內部網路的存取通常是封鎖的，除了避免橫向移動（找到弱點，再轉移到目標主機）之外，也可以避免這類路徑的應用。

Kobako 的網路功能目前仍在開發中，針對這類問題也有做許多考量，解決容易使用問題後，最大的挑戰就落到如何保持安全的使用環境，要在安全、快速和輕量之間平衡，會是相當困難的挑戰。
