Skip to content

fix(cli): restrict ~/.e2b/config.json permissions to owner-only#1320

Open
sebastiondev wants to merge 2 commits into
e2b-dev:mainfrom
sebastiondev:fix/cwe312-user-api-03e6
Open

fix(cli): restrict ~/.e2b/config.json permissions to owner-only#1320
sebastiondev wants to merge 2 commits into
e2b-dev:mainfrom
sebastiondev:fix/cwe312-user-api-03e6

Conversation

@sebastiondev
Copy link
Copy Markdown

Summary

The CLI stores credentials (E2B access token and team API key) in plaintext at ~/.e2b/config.json. Today the file is created with the process default umask, which on most Linux distributions and macOS results in mode 0644 — readable by every other local user and by any process running as a different UID on the same machine.

This PR routes all three write sites through a single writeUserConfig() helper that creates ~/.e2b as 0700 and config.json as 0600, matching the convention used by the AWS CLI (~/.aws/credentials), kubectl (~/.kube/config), and gh (~/.config/gh/hosts.yml).

  • CWE: CWE-312 (Cleartext Storage of Sensitive Information) — partial mitigation. The file remains plaintext on disk (the existing // TODO in user.ts already acknowledges that keychain storage is the proper long-term fix); this change reduces exposure to other local users / less-privileged processes, which is the standard industry mitigation while plaintext storage remains.
  • Affected file: packages/cli/src/user.ts and the three writers in packages/cli/src/commands/.
  • Severity: Moderate on shared / multi-user machines (CI runners, dev VMs, jump boxes); low on single-user workstations.

What's in ~/.e2b/config.json

{
  email, accessToken,           // user access token
  teamName, teamId, teamApiKey  // team API key
}

accessToken authenticates the user against the E2B control plane; teamApiKey authorizes sandbox creation against the team. Either is sufficient to impersonate the user / spend on the team's account.

Fix

A new helper in packages/cli/src/user.ts:

export function writeUserConfig(configPath: string, config: UserConfig): void {
  const dir = path.dirname(configPath)
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
  fs.chmodSync(dir, 0o700)
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 })
  fs.chmodSync(configPath, 0o600)
}

The explicit chmodSync calls are intentional: mkdirSync({ mode }) and writeFileSync({ mode }) only set permissions when the path is created. If the directory or file already exists with looser permissions (the common case for users upgrading), chmodSync corrects them on the next write.

Call sites updated:

  • packages/cli/src/commands/auth/login.ts
  • packages/cli/src/commands/auth/configure.ts
  • packages/cli/src/commands/template/buildWithProxy.ts

logout uses unlinkSync and is unaffected. I grep'd the package for any other writers to USER_CONFIG_PATH — these three are the complete set.

Behavior on Windows: chmodSync only manipulates the read-only bit on Windows, which is consistent with how the AWS/kubectl/gh CLIs behave. ACL hardening on Windows is out of scope for this change.

Tests

Added packages/cli/tests/user_config_permissions.test.ts, which writes a config to a temporary path and asserts the resulting directory is 0700 and file is 0600, plus that the JSON round-trips correctly.

Manually verified before/after on Linux:

# before this PR
$ ls -l ~/.e2b/config.json
-rw-r--r-- 1 user user 234 ... config.json
# after
$ ls -l ~/.e2b/config.json
-rw------- 1 user user 234 ... config.json

Why this is worth fixing

The exploitable scenario is a multi-tenant or shared-account host: another local user (or a process running as nobody, a CI worker UID, a sandboxed app, etc.) can cat ~/<victim>/.e2b/config.json and lift live credentials. No privilege escalation, no race, no special tooling — the file is simply world-readable today.

Before submitting, I tried to disprove the finding: I checked whether E2B sets a restrictive umask anywhere in the CLI bootstrap (it doesn't), whether the tokens are short-lived enough to make disclosure low-impact (the access token isn't visibly rotated and the team API key is long-lived), and whether the directory itself was being created restrictively elsewhere (it wasn't — mkdirSync was called with default mode). None of those mitigations are in place, so the permission tightening is doing real work.

This doesn't close out CWE-312 — that requires moving the secrets out of plaintext entirely, which the existing TODO acknowledges. It does close the "any local user can read it" gap, which is the cheap, high-value half of the mitigation.

Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented May 10, 2026

We require contributors to sign our Contributor License Agreement, and we don't have @sebastiondev on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 10, 2026

⚠️ No Changeset found

Latest commit: 43e169c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3c26d9b92

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +30 to +31
expect(fs.statSync(path.dirname(configPath)).mode & 0o777).toBe(0o700)
expect(fs.statSync(configPath).mode & 0o777).toBe(0o600)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip POSIX permission assertions on Windows

The CLI test workflow runs pnpm test on both ubuntu-22.04 and windows-latest (.github/workflows/cli_tests.yml lines 24-68). On Windows, Node's chmod/stat.mode do not preserve POSIX owner/group/other permission bits, so these masks are not reliably reported as 0o700/0o600 after writeUserConfig; they are typically Windows-derived broad modes such as 0o777/0o666. This new test will therefore fail only on Windows CI even though the config write succeeds, so gate the mode assertions to POSIX or assert Windows behavior separately.

Useful? React with 👍 / 👎.

@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented May 10, 2026

We require contributors to sign our Contributor License Agreement, and we don't have @sebastiondev on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@sebastiondev
Copy link
Copy Markdown
Author

Thanks for the review — you're right, the POSIX bit assertions would fail on windows-latest since Node reports Windows-derived broad modes regardless of chmod.

Fixed in 43e169c: the 0o700/0o600 mask checks are now gated on process.platform !== 'win32', while the JSON content assertion still runs on every platform. The underlying writeUserConfig chmod calls are unchanged (they're effectively no-ops on Windows, which is the existing platform behavior).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant