All tutorials

The sandbox -- running what you do not trust

Cordon 0.6.08 sections · 6 diagramsView this tutorial on GitHub

Every other tutorial rests on one promise: Cordon reads, it never executes. This one is about the single component that breaks that promise on purpose, and about all the ways it is kept away from the rest.

The sandbox -- running what you do not trust, figure 1
┌──────────────────────────────────────────────────────────────────────────┐
│   cordon-scanner                    cordon-sandbox                       │
├──────────────────────────────────────────────────────────────────────────┤
│   reads                             EXECUTES                             │
│   safe on hostile packages          isolation or nothing                 │
│   no flag needed                    --sandbox required, no override      │
│   never sends your code             fetches the artefact                 │
│                                                                          │
│   Two entry points, on purpose. The scanner does not import the          │
│   sandbox, and a test asserts it: a promise with a code path into        │
│   execution is not a promise.                                            │
└──────────────────────────────────────────────────────────────────────────┘
The sandbox -- running what you do not trust, figure 2
┌──────────────────────────────────────────────────────────────────────────┐
│   The static tiers resolve what is constant-derivable:                   │
│                                                                          │
│      aliased imports, bound names, spliced strings, base64 in a          │
│      literal, computed dispatch over a known namespace                   │
│                                                                          │
│   What no static tier can decide is behaviour that only exists           │
│   while the code runs:                                                   │
│                                                                          │
│      a target decoded from a network RESPONSE                            │
│      logic gated on a value that is fetched                              │
│      a payload assembled from two files at import time                   │
│                                                                          │
│   That residual is Rice's theorem, not a missing feature. The            │
│   only instrument that observes it is one that runs the code.            │
└──────────────────────────────────────────────────────────────────────────┘
cordon-sandbox pypi requests==2.31.0 --sandboxcordon-sandbox npm  left-pad          --sandbox --jsoncordon-sandbox rubygems rake          --sandbox

npm, PyPI and RubyGems, each in its own pinned base image.

--sandbox carries no information the command does not already imply. It is there so that running untrusted code is never something you did by accident.

The sandbox -- running what you do not trust, figure 3
┌──────────────────────────────────────────────────────────────────────────┐
│   1. PREPARE      an analysis image is built once, with network,         │
│                   from this project's own Dockerfile. The package        │
│                   under analysis is NOT present at this stage.           │
│                                                                          │
│   2. FETCH        the artefact is downloaded over HTTPS from the         │
│                   registry. Downloading a tarball executes nothing.      │
│                                                                          │
│                   deliberately NOT `pip download`: that runs             │
│                   setup.py egg_info to resolve an sdist, which           │
│                   would run the payload on YOUR host, before any         │
│                   sandbox existed.                                       │
│                                                                          │
│   3. OBSERVE      the artefact is streamed into a container with         │
│                   no network interface and no host filesystem,           │
│                   and installed from the local file with the             │
│                   index disabled.                                        │
│                                                                          │
│                   lifecycle scripts stay ENABLED -- they are the         │
│                   whole reason to run anything, and                      │
│                   --ignore-scripts would produce a clean                 │
│                   observation of a package that was never asked          │
│                   to do the thing.                                       │
└──────────────────────────────────────────────────────────────────────────┘

Some payloads wait: they check the date and do nothing until a day well after anyone looked. So after the first install the sandbox installs again with the clock moved ahead, and reports only what that second run did differently.

   run 1   today            what the install does now   run 2   today + 400 days what it does once a date check passes           ─────────────────────────────────────────────────────   report  run 1, plus "only with the clock 400 days ahead: ..."   --clock-shift DAYS   change the distance;  --clock-shift 0  skips the pass

The clock is moved with libfaketime inside the same isolated container, so the second run has no more access than the first. A trigger that waits on anything other than the date -- a hostname, a count of installs, a message from a server -- is still out of reach.

The sandbox -- running what you do not trust, figure 4
┌──────────────────────────────────────────────────────────────────────────┐
│   GUARANTEED, every run                                                  │
├──────────────────────────────────────────────────────────────────────────┤
│     no network interface                                                 │
│     no host filesystem mounted                                           │
│     a writable layer, discarded afterwards                               │
│     all Linux capabilities dropped                                       │
│     no-new-privileges                                                    │
│     process and memory ceilings                                          │
├──────────────────────────────────────────────────────────────────────────┤
│   STATED, because it varies                                              │
├──────────────────────────────────────────────────────────────────────────┤
│     rootless?      asked of the runtime, never inferred from the         │
│                    binary's name. Podman runs rootful, Docker has        │
│                    a rootless mode; an unreadable answer is              │
│                    reported as rootful.                                  │
│                                                                          │
│     gVisor?        if runsc is configured it is used, and the            │
│                    guest's syscalls are served by a userspace            │
│                    kernel. Without it the HOST KERNEL is the             │
│                    boundary -- a kernel exploit crosses it, which        │
│                    a VM would not allow.                                 │
│                                                                          │
│     syscalls traced?  execve and connect, where available.               │
└──────────────────────────────────────────────────────────────────────────┘

Every one of those is printed with the result, so the report says what the isolation actually was rather than what it usually is.

The sandbox -- running what you do not trust, figure 5
┌──────────────────────────────────────────────────────────────────────────┐
│   If isolation cannot be established, it does not run the code.          │
│                                                                          │
│     not with a warning                                                   │
│     not with reduced isolation                                           │
│     not on the host                                                      │
│     and there is no override flag                                        │
│                                                                          │
│   It exits and says why. A sandbox that degrades to no sandbox is        │
│   worse than none, because the person who asked for it believes          │
│   they are protected.                                                    │
└──────────────────────────────────────────────────────────────────────────┘
  0   nothing observed  1   something was observed  2   the component could not run (no runtime, image build failed)  3   bad invocation (e.g. --sandbox missing)

They match the scanner's, so one pipeline can gate on either.

The sandbox -- running what you do not trust, figure 6
┌──────────────────────────────────────────────────────────────────────────┐
│   scan says CLEAN and you still do not trust it                          │
│      └─▶ a dependency with an install hook you cannot read,              │
│          a minified bundle, a fetched-then-executed target               │
│                                                                          │
│   scan says SUSPECT.OBFUSCATION or DYNAMIC_DISPATCH                      │
│      └─▶ the static tier said 'I cannot resolve this', which is          │
│          a signal, not an answer. The sandbox gives the answer.          │
│                                                                          │
│   you are about to add a dependency to a product                         │
│      └─▶ read it (15), then run it (16). In that order: reading          │
│          is free and cannot hurt you.                                    │
│                                                                          │
│   DO NOT use it as a gate on every build. It is slow, it needs a         │
│   container runtime, and a clean observation only says the               │
│   package did nothing THIS time, on THIS machine.                        │
└──────────────────────────────────────────────────────────────────────────┘

Next: 17 · Source, build systems, binaries and licences.