Kobako: Why the Sandbox Deliberately Has No Time
This article is translated by AI, if have any corrections please let me know.
Once we finished mechanisms like RPC, Handle, and Block, we already had a pleasant mruby scripting environment. But the most critical capability of a sandbox is “security” — every arbitrary script it runs should be treated as dangerous, which means we need to defend against all kinds of scenarios.
Side-Channel Attack
A side-channel attack is a well-known attack technique, and a project like Kobako, which can run full Ruby syntax, is even more exposed to it. Take the following as an example.
1sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))
2
3# ...
4sandbox.eval(<<~SANDBOX)
5 start = Time.now
6 UserRepository.find(email: "...")
7 Time.now - start
8SANDBOX
9
10# => 10ms
11# => 1ms
12# => 1ms
13# => 12msWhen we query four randomly chosen emails one after another, the timings fall into two ranges, around 10ms and around 1ms. From this we can guess that half of the emails probably exist, because a database query that hits takes a different amount of time than one that misses, or because passing through Kobako’s binding takes longer to parse.
Whatever the reason, with enough attempts, or by trying emails already leaked on the internet, an attacker can quickly learn whether those users exist in the system and then move on to further attempts.
This assumes the sandbox is already being abused, but one less attack surface makes the whole project safer, so we have to consider what mechanisms to provide at design time.
Frozen Time
Kobako’s default setting is called the hermetic mode, and it doesn’t include the native mruby-time extension, so nothing related to time exists inside the sandbox.
Simply put, if a script wants to ask for the time, it can’t, because there is no Time. Even if it tries to measure in some other way, all the sandbox sees is “how many units have passed” rather than an exact number of seconds or milliseconds, so the chance of exploiting a side-channel attack becomes very low.
However, many scenarios still need “time” to exist. In most of them we don’t need high precision — at least nobody asks questions like “at what hour, minute, and second did this user sign up?” Filtering users usually only needs precision down to the day, so we can simply use the existing binding (Bind) capability to provide a low-precision time.
1sandbox.bind("Date::Today", -> { Date.today.to_s })With this, any attempt to measure “whether a user exists” fails because the granularity of “one day” is too coarse — no amount of trying can confirm it, which avoids the problem to a certain degree.
Of course, most scenarios don’t need such an extreme design. Providing time down to the second is usually enough to make large-scale abuse and probing hard; before that happens, the attempts are likely to be blocked by sandbox limits or rate limiting anyway.
1sandbox.bind("CurrentTime", -> { Time.now.to_i })A design like this is basically more than enough for most scenarios.
Security Trade-off
This fully sealed design was basically safe in Kobako’s early use cases, but to support AI use cases we recently started supporting a Virtual Filesystem and Networking, which brings these risks back into the project.
1sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))
2
3# ...
4sandbox.eval(<<~SANDBOX)
5 File.write('/mnt/.timestamp', '')
6 start = File.mtime('/mnt/.timestamp')
7 UserRepository.find(email: "...")
8 File.write('/mnt/.timestamp', '')
9 File.mtime('/mnt/.timestamp') - start
10SANDBOXOnce we can access files, a new hole opens right away: the file’s modification time is exposed, and in the end time can still be inferred.
That’s why, when designing the virtual filesystem, Kobako pins mtime (modification time) to the start of 1970 (the Epoch) under hermetic mode, which closes off this path.
Still, there are plenty of other possibilities, such as this approach:
1sandbox.eval(<<~SANDBOX)
2 start = Net::HTTP.get('https://hacker.com/timestamp').to_i
3 UserRepository.find(email: "...")
4 Net::HTTP.get('https://hacker.com/timestamp').to_i - start
5SANDBOXSuppose there is a server that reliably returns the current time; the program in the sandbox only needs to send it requests. In practice this is considerably harder, though. Unless the network path happens to be very close, the response can’t come back in a short enough time, and the difference most likely can’t be measured.
Another possible path is querying the host environment, where latency usually stays within 1ms and could be exploited. This is why most sandboxes block access to the internal network: besides preventing lateral movement (finding a weakness, then moving on to the target host), it also prevents this kind of path.
Kobako’s networking feature is still in development, and we’ve given these problems a lot of thought. Once the ease-of-use problems are solved, the biggest challenge becomes keeping the environment safe to use — balancing security, speed, and lightness will be a difficult challenge.
Enjoyed this article? Buy me a milk tea 🧋