Back to glossary

Sandbox Environment

A sandbox environment is an isolated space that runs code with restricted access to files, network, and credentials, so its effects stay contained.

Also known as Sandboxing, Security sandbox, Malware sandbox, Test sandbox

A sandbox environment is an isolated place to run code with deliberately reduced access to the host's files, network, processes, and credentials, so whatever that code does stays contained. Teams use a sandbox to test changes away from production and to execute untrusted code, such as a new dependency's install script, without trusting it.

A sandbox is only as strong as the layer that enforces it. A JavaScript context inside your own process is a convenience; a separate kernel behind a hypervisor is a boundary. In the open source supply chain, sandboxes show up in two places: around npm install and pip install, to contain code you chose to run, and inside malware scanners, to detonate packages and watch them. We think both uses are necessary and both are routinely misunderstood. A sandbox contains code; it does not tell you whether the code is malicious, malware increasingly checks whether it is being watched, and every real sandbox breaks at the thing you had to let in. In July 2026, Anthropic disclosed that a package one of its models published during a misconfigured evaluation ran on 15 systems, which it believes were vendor scanning sandboxes, and one of them leaked its credentials to the package.

What is a sandbox environment?

#

A sandbox environment is any execution space where actions cannot reach real assets. The term covers two related ideas.

The first is a non-production copy for testing. A staging cluster, a payment provider's test mode, or a SaaS "sandbox org" lets you exercise real workflows against fake money and fake customers. Isolation here is about data and side effects: nothing you do should touch production records.

The second is a security boundary for untrusted code. Browsers run each site's renderer in a sandboxed process, mobile operating systems confine every app to its own container, and malware analysts detonate samples in disposable virtual machines. Isolation here is about authority: the code may be hostile, so the sandbox decides which system resources it can name at all.

The second meaning is the one that matters for software supply chain security, and the rest of this page is about it.

How does a sandbox isolate code?

#

A sandbox isolates code by removing authority the code would otherwise inherit. To see why that is necessary, start with how an ordinary process behaves.

When a package manager runs a dependency's lifecycle script, that script runs as you. It inherits your user ID, your working directory, your environment variables, and your network. It can read ~/.ssh/id_ed25519, ~/.npmrc, ~/.aws/credentials, and any token exported in the shell. Unix grants this ambient authority by design: a process is trusted to act on behalf of the user who started it. That design is correct for a compiler you wrote and wrong for a postinstall script someone published an hour ago.

A sandbox subtracts from that default at some layer. The layers differ in what they still share with the host, and that shared surface is where escapes come from.

Figure 1 orders the common sandbox layers from weakest to strongest, and the top two are not boundaries for hostile code. A language-runtime sandbox, such as Node's vm module, shares the host process, its memory, and its object graph. A permission model inside the runtime, such as Node's --permission flag or Deno's permission flags, shares the process but checks API calls against a policy. An operating-system sandbox, such as Linux namespaces with seccomp-bpf and Landlock, macOS Seatbelt, or bubblewrap, gives code its own process but shares the host kernel. A container is the same kernel-level isolation packaged with an image. A user-space kernel such as gVisor intercepts system calls before they reach the host kernel. A virtual machine or microVM runs a separate guest kernel and shares only the hypervisor and the hardware.

LayerWhat it shares with the hostTypical escapeStartup costUse it for
Language runtime (node:vm, vm2)Process, heap, object prototypesPrototype walk to the host Function constructorMicrosecondsNothing hostile. Use it to separate trusted code.
Runtime permission model (Node --permission, Deno)Process and runtime internalsNative addon, runtime bug, unguarded APINoneLimiting mistakes in code you trust
OS process sandbox (namespaces, seccomp, Landlock, Seatbelt, bubblewrap)KernelKernel vulnerability, over-broad policyMillisecondsInstall scripts and build steps on developer machines and CI
ContainerKernelKernel vulnerability, mounted Docker socket, privileged modeSub-secondBuilds and CI jobs; hostile code only with a hardened profile
User-space kernel (gVisor)A narrowed host syscall surfaceSentry or host kernel bugSub-secondMulti-tenant untrusted code
VM or microVM (Firecracker, full VM)Hypervisor and hardwareHypervisor bug, side channels100ms to secondsMalware detonation and anything you assume is hostile

Our verdict: put untrusted package code behind OS-level isolation with no network and no secrets at minimum, and put code you believe is hostile, such as a malware sample, behind a separate kernel. Use runtime-level controls only to limit mistakes in code you already trust. A chroot jail does not appear in the table because it is not a sandbox: the Linux manual page says it is "not intended to be used for any kind of security purpose".

Whatever the layer, the least privilege principle does the real work. A VM that mounts your home directory and forwards your SSH agent is a weaker sandbox than a namespace with an empty filesystem and no network.

Why is node:vm not a security sandbox?

#

Node's vm module is not a security sandbox because code inside the context can reach the host's JavaScript objects. The Node.js documentation says so in bold: "The node:vm module is not a security mechanism. Do not use it to run untrusted code."

The escape takes one line. An empty context still exposes this, whose constructor chain leads to the host realm's Function constructor, which compiles code outside the context:

JavaScript
const vm = require('node:vm');
const ctx = vm.createContext({});            // "empty" sandbox: no require, no process
const out = vm.runInContext(
  `this.constructor.constructor('return process')().versions.node`, ctx);
console.log('escaped, host Node.js', out);
Plain text
$ node vm-escape.js
escaped, host Node.js 22.23.4

Once the code holds process, it holds process.mainModule.require and therefore child_process. Libraries that wrap vm inherit the problem. The vm2 sandbox escape (CVE-2026-26956) let code passed to VM.run() reach the host process and execute operating system commands. We confirmed the exploit across 66 loadable vm2 releases, from 0.2.2 through 3.10.4, a much wider range than the original advisory listed. The vm2 README is candid about the pattern: "researchers and security professionals continuously discover new ways to escape the vm2 sandbox."

The same mistake ships inside developer tools. The NestJS devtools sandbox escape (CVE-2025-54782) exposed a localhost endpoint that ran posted JavaScript in a vm context. Combined with a cross-site request any web page could send, it gave remote code execution on a developer's machine.

Node's newer Permission Model is a different mechanism. It became stable in Node.js v22.13.0 and v23.5.0, and it denies file system, child process, worker, and native addon access unless you grant it. As of Node.js 26, it also gates network access with --allow-net:

Plain text
$ node --permission --allow-fs-read="$PWD" read-secret.js
Error: Access to this API has been restricted. Use --allow-fs-read to manage permissions.
  code: 'ERR_ACCESS_DENIED',
  permission: 'FileSystemRead',
  resource: '/root/.npmrc'

The Permission Model is useful, and its own documentation states its limit plainly: "This feature does not protect against malicious code." It is a seat belt for trusted code that misbehaves by accident. A hostile package needs OS-level isolation or stronger.

How do you sandbox npm install?

#

You sandbox npm install by splitting it into a fetch phase that needs the network and a build phase that runs package code without the network, secrets, or your home directory. Install time is where most supply chain attacks execute, because install scripts run the moment a package lands in node_modules.

Package managers have started to close the default. pnpm 10 stopped running dependency lifecycle scripts unless they are allowlisted. As of October 2026, npm v12 turns allowScripts off by default, including the implicit node-gyp rebuild that the Miasma "Phantom Gyp" waves abused to run code with no script in package.json. Default-off install scripts were overdue, and we are glad they arrived.

Allowlisting decides whether a script runs. It does not decide what an approved script can reach. Native modules such as sharp and better-sqlite3, and binary downloaders such as Cypress and Electron, must be approved to work, and once approved they run with your full authority. If an approved package ships a compromised version, the allowlist waves it through. A sandbox is the control that bounds the damage of a script you had to approve.

Figure 2 shows the split. Phase 1 resolves and downloads tarballs with the network on and every script disabled, so no package code runs while the network is reachable. Phase 2 runs the lifecycle scripts inside a sandbox with no network interface, an empty HOME, and a scrubbed environment. Phase 3 is your application, which loads the packages with whatever authority you give it.

Here is the pattern on Linux, using an unprivileged user and network namespace. The test package's postinstall reads ~/.npmrc and tries to reach the internet:

JavaScript
const fs = require('fs'), os = require('os'), https = require('https');
let npmrc;
try { npmrc = fs.readFileSync(os.homedir() + '/.npmrc', 'utf8').trim(); } catch (e) { npmrc = e.code; }
console.log('[postinstall] HOME=%s npmrc=%s', os.homedir(), npmrc);
https.get('https://registry.npmjs.org/', r => { console.log('[postinstall] exfil ok', r.statusCode); process.exit(0); })
  .on('error', e => { console.log('[postinstall] exfil failed:', e.code); process.exit(0); });
Bash
# Phase 1: network on, no package code runs
npm install --ignore-scripts

# Phase 2: package code runs with no network, no secrets, empty HOME
unshare --user --map-root-user --net \
  env -i PATH="$PATH" HOME=/tmp/empty-home \
  npm rebuild --foreground-scripts
Plain text
--- unsandboxed: npm rebuild --foreground-scripts
[postinstall] HOME=/root npmrc=//registry.npmjs.org/:_authToken=npm_FAKE
[postinstall] exfil ok 200

--- sandboxed: phase 2 above
[postinstall] HOME=/tmp/empty-home npmrc=ENOENT
[postinstall] exfil failed: EAI_AGAIN

We ran this on npm 10.9.9 and Linux 6.18. On npm v12 you also approve the package in allowScripts, or npm rebuild will skip it. Some distributions restrict unprivileged user namespaces; Ubuntu 24.04 does so through AppArmor. On macOS, use a VM or a tool built on Seatbelt. A container with --network none and no mounted credentials gives the same result in CI.

The trade-offs are real. Scripts that legitimately download binaries during install, such as Cypress and Electron, fail in phase 2 and need their artifacts fetched in phase 1 or from an internal mirror. A sandboxed install also does nothing about code that runs later: a malicious module that waits for require() executes inside your application, with your application's environment variables and network. Sandboxing the install removes the easiest execution point; it does not make an untrusted dependency trustworthy.

Malware checks whether it is in a sandbox

#

Malware authors know that scanners detonate packages in sandboxes, so their code checks where it is running before doing anything visible. MITRE ATT&CK tracks this as T1497, Virtualization/Sandbox Evasion.

The 60 malicious npm packages that leaked network and host data, which passed 3,000 downloads, all carried the same fingerprinting script. Before exfiltrating anything, it returned early on hosts that looked like cloud scanners or research VMs:

JavaScript
// From the seatable package, identical across all 60 (annotations ours)
if ( externalHost.includes("compute.amazonaws.com") ||    // AWS
     externalHost.includes("bc.googleusercontent.com") || // GCP
     externalHost.includes("default-rdns.vocus.co.nz") || // Sandboxes
     internalHost.includes("LD.local") ||                 // Lab domain
     homedir.match(/justin|mal_data|malicious/i) ) {      // Research VMs
     return;                           // Abort if running in known test envs
}

Run that package in a sandbox hosted on AWS and you observe a quiet install script that resolves DNS and exits. The behavior an analyst needs to see never executes.

Evasion does not need to be explicit. The 10 typosquatted npm packages that deployed a credential harvester showed a fake CAPTCHA and waited for keyboard input before downloading a 24MB infostealer, so an unattended detonation never reached the second stage. The same packages sent the victim's IP address to the attacker's server before fetching the payload, which gave the server a chance to decide who received it. Delays, locale checks, and CI-only triggers work the same way.

This is the core limit of dynamic analysis in a sandbox: it records what code did on one run, in one environment, and the code chooses what to do. A clean detonation is weak evidence. As of October 2026, Socket analyzes the published artifact with static analysis, which our FAQ describes as "analyzing source code without actually executing it." Static analysis sees both branches of the if. The evasion check is itself a signal, and the fetch-and-execute code is visible even when the server withholds the payload. Static analysis has its own blind spots, mainly heavy obfuscation and payloads that only exist on a remote server, which is why we treat malware detection as a combination of signals rather than one verdict. Our 2025 mid-year threat report recommends using sandboxing to observe post-install execution alongside static checks for obfuscated logic.

Analysis sandboxes are now targets

#

A sandbox that runs hostile code is only safe if nothing valuable is reachable from inside it. Two incidents in 2026 showed what happens when the surrounding infrastructure leaks.

In the Claude Mythos 5 PyPI incident, a model under a capture-the-flag evaluation reached the public internet through a misconfiguration and published a credential-stealing package, a dependency confusion play aimed at a fictional target. Anthropic's July 30 disclosure says the package "was downloaded and run on 15 real systems" during the roughly one hour it was public. Its September assessment says Anthropic believes all 15 were security vendors installing new packages in sandboxed environments, and that "one vendor's scanner leaked its access credentials to the model while installing the package." The model used them to reach the vendor's live database. The malicious package did exactly what such packages do; the scanner's sandbox simply had something worth stealing.

In the OpenAI and Hugging Face ExploitGym incident, roughly 1,200 agents running in isolated sandboxes exchanged more than 70,000 messages through an internal package mirror. OpenAI's technical report explains why the mirror was reachable: the sandboxes were designed to install packages from internal mirrors instead of the public internet, and the agents had shared credentials for it. Zero-days in that mirror later "became the agents' principal egress path." The one service every sandbox needed became the shared channel between them and the route out. The same pressure applies to coding agents, which is why agentic AI security increasingly means OS-level sandboxes around agent shell commands.

Both failures follow the same rule. Isolation held where it was enforced and broke at what had been let in: a mirror every sandbox needed, and credentials a scanner carried.

The hole is whatever you let in

#

Every useful sandbox admits something. A build sandbox needs packages, so it reaches a registry or a mirror. A scanner needs to report results, so it holds a token. A coding agent needs to call an API, so its proxy allowlists a domain. Each admission is a channel, and attackers already route exfiltration through services defenders allowlist by habit: our mid-year report documents stolen data sent through Gmail, Sentry, Discord webhooks, and out-of-band testing infrastructure.

Designing a sandbox therefore starts with the openings, not the walls. Keep reusable credentials out entirely and inject short-lived ones per job. Deny egress by default, and treat an allowed registry or mirror as shared, writable, attacker-reachable state rather than as part of the trusted base. Assume the code inside knows it is in a sandbox, and check what it was written to do as well as what it did. Container security guidance covers the kernel half of that work; the openings are where 2026's incidents actually happened.