# 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

Source: https://socket.dev/glossary/sandbox-environment

Last updated: 2026-10-31

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. Each layer is defined by what the sandboxed code still shares with the host. Escapes come from the shared part.
>
> Sandbox isolation layers from weakest to strongest, and what each shares with the host

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.

| Layer | What it shares with the host | Typical escape | Startup cost | Use it for |
| --- | --- | --- | --- | --- |
| Language runtime (node:vm, vm2) | Process, heap, object prototypes | Prototype walk to the host Function constructor | Microseconds | Nothing hostile. Use it to separate trusted code. |
| Runtime permission model (Node --permission, Deno) | Process and runtime internals | Native addon, runtime bug, unguarded API | None | Limiting mistakes in code you trust |
| OS process sandbox (namespaces, seccomp, Landlock, Seatbelt, bubblewrap) | Kernel | Kernel vulnerability, over-broad policy | Milliseconds | Install scripts and build steps on developer machines and CI |
| Container | Kernel | Kernel vulnerability, mounted Docker socket, privileged mode | Sub-second | Builds and CI jobs; hostile code only with a hardened profile |
| User-space kernel (gVisor) | A narrowed host syscall surface | Sentry or host kernel bug | Sub-second | Multi-tenant untrusted code |
| VM or microVM (Firecracker, full VM) | Hypervisor and hardware | Hypervisor bug, side channels | 100ms to seconds | Malware 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"](https://man7.org/linux/man-pages/man2/chroot.2.html).

Whatever the layer, the [least privilege principle](https://socket.dev/glossary/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](https://nodejs.org/api/vm.html) 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);
```

```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)](https://socket.dev/blog/free-certified-patches-for-critical-vm2-sandbox-escape) let code passed to `VM.run()` reach the host process and execute operating system commands. We confirmed the exploit across 66 loadable [vm2](https://socket.dev/npm/package/vm2) releases, from 0.2.2 through 3.10.4, a much wider range than the original advisory listed. The [vm2 README](https://github.com/patriksimek/vm2) 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)](https://socket.dev/blog/nestjs-rce-vuln) 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](https://nodejs.org/api/permissions.html) 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`:

```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](https://socket.dev/glossary/postinstall-scripts) run the moment a package lands in `node_modules`.

Package managers have started to close the default. [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) stopped running dependency lifecycle scripts unless they are allowlisted. As of October 2026, [npm v12](https://socket.dev/blog/npm-12) turns `allowScripts` off by default, including the implicit `node-gyp rebuild` that the [Miasma](https://socket.dev/supply-chain-attacks/miasma-mini-shai-hulud-supply-chain-attack) "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. No phase has both untrusted install code and the network. Phase 3 is outside the install sandbox: code that waits for require() runs with your application's authority.
>
> Two-phase sandboxed npm install: fetch with network and no scripts, build with scripts and no network, then run

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); });
```

```sh
# 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
```

```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](https://socket.dev/glossary/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](https://attack.mitre.org/techniques/T1497/).

The [60 malicious npm packages that leaked network and host data](https://socket.dev/blog/60-malicious-npm-packages-leak-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](https://socket.dev/blog/10-npm-typosquatted-packages-deploy-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](https://docs.socket.dev/docs/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](https://socket.dev/glossary/malware-detection) as a combination of signals rather than one verdict. Our [2025 mid-year threat report](https://socket.dev/blog/malicious-open-source-packages-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](https://socket.dev/blog/claude-pypi-attack), a model under a capture-the-flag evaluation reached the public internet through a misconfiguration and published a credential-stealing package, a [dependency confusion](https://socket.dev/glossary/dependency-confusion) play aimed at a fictional target. Anthropic's [July 30 disclosure](https://www.anthropic.com/research/investigating-incidents-cybersecurity-evals) says the package "was downloaded and run on 15 real systems" during the roughly one hour it was public. Its [September assessment](https://www.anthropic.com/research/alignment-assessment-cybersecurity-incidents) 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](https://socket.dev/glossary/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](https://socket.dev/blog/autonomous-agents-escape), roughly 1,200 agents running in isolated sandboxes exchanged more than 70,000 messages through an internal package mirror. OpenAI's [technical report](https://cdn.openai.com/pdf/67869394-cb91-4c12-888c-5cbd85c7814c/OpenAI-Hugging-Face%20Incident-Technical-Report.pdf) 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](https://socket.dev/glossary/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](https://socket.dev/glossary/container-security) guidance covers the kernel half of that work; the openings are where 2026's incidents actually happened.
