|
| 1 | +# OpenBoot Threat Model |
| 2 | + |
| 3 | +**Version:** as of repository HEAD |
| 4 | +**Scope:** `openboot` CLI, macOS only |
| 5 | +**Audience:** Security-minded developers evaluating OpenBoot for personal or team use |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. Scope |
| 10 | + |
| 11 | +### What OpenBoot Does |
| 12 | + |
| 13 | +OpenBoot is a macOS CLI that automates developer environment setup. In a single invocation it can: |
| 14 | + |
| 15 | +- Install Homebrew formulae and casks (by name, via `brew install`) |
| 16 | +- Install npm global packages (by name, via `npm install -g`) |
| 17 | +- Add Homebrew taps (by owner/repo name, via `brew tap`) |
| 18 | +- Install Oh-My-Zsh and patch `~/.zshrc` with a theme and plugin list |
| 19 | +- Apply macOS `defaults write` preferences |
| 20 | +- Clone a dotfiles git repository and run `stow` to link files |
| 21 | +- Run an arbitrary post-install shell script sourced from a remote config (opt-in only) |
| 22 | +- Capture and restore snapshots of the above state |
| 23 | + |
| 24 | +### What OpenBoot Does Not Do |
| 25 | + |
| 26 | +- It does not escalate privileges with `sudo` directly. Any privilege escalation that occurs happens inside Homebrew or Xcode CLT installers, which request it themselves. |
| 27 | +- It does not store credentials other than a single bearer token in `~/.openboot/auth.json`. |
| 28 | +- It does not phone home with telemetry, package lists, or usage data. |
| 29 | +- It does not execute remote shell code by default. The `post_install` field in a remote config is skipped unless the operator explicitly passes `--allow-post-install` (in non-interactive mode) or confirms a prompt (in interactive mode). |
| 30 | +- It does not modify files outside the user's home directory, except through Homebrew or Xcode which manage their own prefix paths. |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## 2. Actors and Trust Levels |
| 35 | + |
| 36 | +| Actor | Trust Level | Rationale | |
| 37 | +|---|---|---| |
| 38 | +| **Local user running `openboot`** | Full trust | The CLI runs as the user. All actions are bounded by the user's own file permissions. | |
| 39 | +| **openboot.dev API** | High, with validation | All responses are validated against a schema (`RemoteConfig.Validate()`). Response body size is capped at 1 MiB (`io.LimitReader`). The API URL is pinned to HTTPS (configurable only to HTTPS or localhost). | |
| 40 | +| **Remote config author** (`openboot install <user/slug>`) | Medium — verified identity, untrusted content | The config author is an authenticated openboot.dev user. Their package names are regex-validated. Their `post_install` commands are not executed without explicit opt-in. | |
| 41 | +| **Oh-My-Zsh GitHub CDN** (`raw.githubusercontent.com`) | Accepted risk, no hash verification | The OMZ install script is fetched and executed via `curl | sh`. No checksum is verified. This is the official OMZ install method. | |
| 42 | +| **Homebrew** | High | Homebrew is treated as a trusted package manager. Package integrity is managed by Homebrew's own SHA-256 verification. OpenBoot only passes package names to `brew install`. | |
| 43 | +| **npm registry** | Medium | npm packages are installed by name with no additional pinning beyond whatever `npm` itself enforces (no lockfile context at install time). | |
| 44 | +| **Dotfiles repository** | Untrusted until user confirms | Cloned from a URL supplied by the config or the user. The URL is validated to use HTTPS. Content of the cloned repo is not scanned. | |
| 45 | +| **GitHub Releases** (auto-update) | High, no hash verification | Binary downloads come from `github.com/openbootdotdev/openboot/releases`. No checksum is verified after download. The binary is written over the current executable. | |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## 3. Threat Scenarios |
| 50 | + |
| 51 | +### T1 — Malicious Remote Config: Post-Install Code Execution |
| 52 | + |
| 53 | +**Description:** A remote config hosted on openboot.dev includes a `post_install` array containing arbitrary shell commands. If a user runs `openboot install attacker/slug`, those commands could be executed. |
| 54 | + |
| 55 | +**Likelihood:** Medium. Any authenticated openboot.dev user can publish a config with `post_install`. |
| 56 | + |
| 57 | +**Impact:** Critical. `post_install` is passed verbatim to `/bin/zsh -c` and runs as the user, in their home directory, with full user privileges. |
| 58 | + |
| 59 | +**Mitigations:** |
| 60 | + |
| 61 | +- In interactive mode (`--silent` not set, TTY present), `stepPostInstall` shows a preview of every command in the script and requires explicit `y` confirmation before executing (`ui.Confirm`). A user who reads the preview can reject it. |
| 62 | +- In non-interactive / silent mode, `post_install` is **skipped by default**. Execution only occurs if the caller also passes `--allow-post-install`. This flag is not set by default in any automated invocation. |
| 63 | +- The preview is always shown before prompting, so the user sees what will run. |
| 64 | + |
| 65 | +**Residual risk:** The gate is a text preview and a confirmation prompt. A user who does not read the preview, or who runs `--allow-post-install` without reviewing the config, will execute the commands. There is no sandbox, no allowlist, and no signature verification on `post_install` content. The field is inherently a remote code execution primitive behind a user-approval gate. |
| 66 | + |
| 67 | +**Recommendation for teams:** If you are deploying openboot in a CI or fleet context, never pass `--allow-post-install` unless you control the config author's account and have reviewed the commands. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +### T2 — Malicious Remote Config: Package Name Injection |
| 72 | + |
| 73 | +**Description:** A remote config specifies package names that are passed to `brew install`, `brew install --cask`, `npm install -g`, or `brew tap`. An attacker-controlled config could attempt to install malicious packages or exploit shell metacharacters in names to inject arguments. |
| 74 | + |
| 75 | +**Likelihood:** Low. The validation layer is a meaningful barrier. |
| 76 | + |
| 77 | +**Impact:** High if bypassed. A malicious cask or formula could install persistent malware. |
| 78 | + |
| 79 | +**Mitigations:** |
| 80 | + |
| 81 | +- `RemoteConfig.Validate()` applies regex allowlists before any package name is used: |
| 82 | + - Formula and cask names: `^[a-zA-Z0-9@/_.-]+$` (max 200 chars) |
| 83 | + - Tap names: `^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$` (exactly `owner/repo`) |
| 84 | + - npm package names: same regex as formulae |
| 85 | +- Package names are passed as discrete arguments to `exec.Command("brew", "install", ...)`, not interpolated into a shell string. Shell metacharacters that survive the regex (none should) would not be interpreted by the shell. |
| 86 | +- Homebrew itself checks package existence and integrity (SHA-256 of bottles). |
| 87 | + |
| 88 | +**Residual risk:** Package squatting. A legitimate-looking package name that passes regex validation could still be a malicious Homebrew formula or npm package. OpenBoot does not verify that package names correspond to known-safe packages. Defense against typosquatting is the user's responsibility. |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +### T3 — Malicious Dotfiles Repository |
| 93 | + |
| 94 | +**Description:** A remote config specifies a `dotfiles_repo` URL pointing to an attacker-controlled git repository. When cloned and stowed, the dotfiles content runs during the next shell session. |
| 95 | + |
| 96 | +**Likelihood:** Medium. Any remote config can specify a dotfiles URL. |
| 97 | + |
| 98 | +**Impact:** High. Dotfiles applied to `~` can inject shell hooks, PATH entries, aliases, or git credential helpers that execute code at login or on git operations. |
| 99 | + |
| 100 | +**Mitigations:** |
| 101 | + |
| 102 | +- `ValidateDotfilesURL` enforces: |
| 103 | + - HTTPS scheme only (no `git@`, no `http://`, no `file://`) |
| 104 | + - Hostname must be present |
| 105 | + - Path must not contain `..` or `//` |
| 106 | + - Path structure: `/<owner>/<repo>` (at most two segments with alphanumeric/dot/dash/underscore) |
| 107 | + - Maximum 500 characters |
| 108 | +- The validation prevents file-scheme local path reads and path traversal in the URL itself. |
| 109 | +- In interactive mode, the user is shown the package list (including `dotfiles_repo` via config display) before confirming. |
| 110 | + |
| 111 | +**Residual risk:** Validation only constrains the URL form, not the repository content. Any HTTPS git URL that satisfies the regex is accepted. The dotfiles repo content is fully trusted once cloned. A user who installs a config from an untrusted author is trusting that author's dotfiles repo. |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +### T4 — Oh-My-Zsh Install via curl | sh |
| 116 | + |
| 117 | +**Description:** `InstallOhMyZsh` fetches `https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh` and pipes it directly to `bash`. No checksum is verified. |
| 118 | + |
| 119 | +**Likelihood:** Low (requires GitHub or CDN compromise). |
| 120 | + |
| 121 | +**Impact:** Critical if the script is tampered with. The script runs as the user and performs arbitrary operations. |
| 122 | + |
| 123 | +**Mitigations:** |
| 124 | + |
| 125 | +- The URL is hardcoded to `raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/...`. The domain is not user-configurable. |
| 126 | +- HTTPS is used. TLS certificate validation is performed by the system's `curl`. |
| 127 | +- This is the officially documented install method for Oh-My-Zsh. The project does not provide a signed release artifact. |
| 128 | + |
| 129 | +**Accepted risk:** There is no hash pinning. A compromise of the GitHub repository, CDN, or a TLS MitM would not be detected. This is the same risk accepted by every other tool that uses the official OMZ installer. It is listed here for transparency, not because there is a viable alternative that the OMZ project provides. |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +### T5 — Auto-Update Binary Replacement Without Hash Verification |
| 134 | + |
| 135 | +**Description:** When not installed via Homebrew, `DownloadAndReplace` fetches the latest release binary from `github.com/openbootdotdev/openboot/releases/latest/download/openboot-darwin-<arch>` and atomically replaces the running executable. No checksum is verified after download. |
| 136 | + |
| 137 | +**Likelihood:** Low (requires GitHub infrastructure compromise or TLS MitM). |
| 138 | + |
| 139 | +**Impact:** Critical. A tampered binary would run with full user privileges on every subsequent invocation. |
| 140 | + |
| 141 | +**Mitigations:** |
| 142 | + |
| 143 | +- The download URL is hardcoded to `github.com`. It is not user-configurable. |
| 144 | +- HTTPS is used with system TLS. |
| 145 | +- The replacement is atomic (`os.Rename` from a `.tmp` file), so a failed download does not corrupt the existing binary. |
| 146 | +- Homebrew installs (the primary distribution channel) use `brew upgrade`, which verifies the formula's SHA-256 bottle checksum. |
| 147 | +- Auto-update can be disabled with `OPENBOOT_DISABLE_AUTOUPDATE=1` or by setting `~/.openboot/config.json` `autoupdate` to `"false"` or `"notify"`. |
| 148 | + |
| 149 | +**Residual risk:** Direct binary installs (non-Homebrew) have no post-download integrity check. The `OPENBOOT_UPGRADING=1` guard prevents infinite re-exec loops but does not authenticate the downloaded binary. |
| 150 | + |
| 151 | +**Recommendation:** Prefer Homebrew installation, which provides formula-level SHA-256 verification. If using direct binary install in a managed fleet, set `OPENBOOT_DISABLE_AUTOUPDATE=1` and control updates out-of-band. |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +### T6 — Auth Token Exposure |
| 156 | + |
| 157 | +**Description:** The bearer token issued by openboot.dev is stored in `~/.openboot/auth.json`. If this file is read by another process or included in a backup that leaks, the token grants API access as the authenticated user. |
| 158 | + |
| 159 | +**Likelihood:** Low on a well-administered machine. |
| 160 | + |
| 161 | +**Impact:** Medium. An attacker with the token can push configs or snapshots as the victim user. They cannot run code on the victim's machine with the token alone. |
| 162 | + |
| 163 | +**Mitigations:** |
| 164 | + |
| 165 | +- `SaveToken` creates `~/.openboot/auth.json` with permissions `0600` (owner read/write only). |
| 166 | +- The `~/.openboot/` directory is created with `0700`. |
| 167 | +- Tokens have an `expires_at` timestamp. `LoadToken` returns `nil` for expired tokens, and the server enforces expiry on the API side. |
| 168 | +- `openboot logout` calls `DeleteToken`, which removes the file. |
| 169 | + |
| 170 | +**Residual risk:** Root processes on the machine can read any file regardless of permissions. Processes running as the same user can also read the token. This is the standard threat model for any bearer-token CLI credential store (similar to `~/.npmrc`, `~/.netrc`, or AWS CLI credentials). |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +### T7 — macOS Preferences Injection |
| 175 | + |
| 176 | +**Description:** A remote config can include `macos_prefs` — an array of `defaults write` commands applied to arbitrary domains. A malicious config could write values that affect security-relevant system preferences. |
| 177 | + |
| 178 | +**Likelihood:** Medium. Any config author can include `macos_prefs`. |
| 179 | + |
| 180 | +**Impact:** Medium. `defaults write` operates on user-space preferences. It cannot modify system-level settings that require SIP bypass or root. However, some user preferences affect security behavior (e.g., Gatekeeper user overrides, quarantine flags, screensaver lock settings). |
| 181 | + |
| 182 | +**Mitigations:** |
| 183 | + |
| 184 | +- `RemoteConfig.Validate()` checks that `macos_prefs[*].type` is one of `string`, `int`, `bool`, `float`, or empty. This prevents type confusion but does not restrict domain/key pairs. |
| 185 | +- `Configure` passes each preference as discrete arguments to `exec.Command("defaults", "write", domain, key, ...)`. Values are not shell-interpolated. |
| 186 | +- In interactive mode, the user confirms the full package list before installation begins. |
| 187 | + |
| 188 | +**Residual risk:** There is no allowlist of permitted `defaults write` domains or keys. A config author can set any user-writable preference. Users should not apply configs from authors they do not trust. |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +### T8 — OPENBOOT_API_URL Redirection |
| 193 | + |
| 194 | +**Description:** The `OPENBOOT_API_URL` environment variable overrides the API base URL. If an attacker can set environment variables before running `openboot` (e.g., via a compromised `.zshrc` that a prior dotfiles install wrote), they could redirect API calls to an attacker-controlled server. |
| 195 | + |
| 196 | +**Likelihood:** Very low in normal use. |
| 197 | + |
| 198 | +**Impact:** High if exploited. A malicious API server could return crafted configs, capture the auth token sent in `Authorization` headers, or respond with packages that exploit the validation boundary. |
| 199 | + |
| 200 | +**Mitigations:** |
| 201 | + |
| 202 | +- `IsAllowedAPIURL` in `internal/system/apiurl.go` rejects any URL that is not `https://` or `http://localhost` / `http://127.0.0.1` / `http://[::1]`. Plaintext HTTP to non-localhost addresses is rejected. |
| 203 | +- Both `config.getAPIBase()` and `auth.GetAPIBase()` apply this check and log a warning before falling back to `https://openboot.dev`. |
| 204 | + |
| 205 | +**Residual risk:** An attacker who already controls the user's shell environment has many more effective attack vectors. This mitigation is adequate for the threat. |
| 206 | + |
| 207 | +--- |
| 208 | + |
| 209 | +### T9 — Snapshot Import from Untrusted URL |
| 210 | + |
| 211 | +**Description:** `openboot snapshot --import <url>` accepts an HTTPS URL and fetches a snapshot file. The content is parsed as a `RemoteConfig` or snapshot and can trigger package installation, dotfiles cloning, and macOS pref writes. |
| 212 | + |
| 213 | +**Likelihood:** Low (requires user to be socially engineered into running the command with an attacker URL). |
| 214 | + |
| 215 | +**Impact:** High. Same impact surface as T1–T3. |
| 216 | + |
| 217 | +**Mitigations:** |
| 218 | + |
| 219 | +- The same `ValidateDotfilesURL` and `RemoteConfig.Validate()` paths run on imported content. |
| 220 | +- Response body is capped at 1 MiB. |
| 221 | +- Snapshot files imported via `--import` do not contain `post_install` (the `loadSnapshotAsRemoteConfig` function does not populate that field; a code comment explicitly notes this). |
| 222 | + |
| 223 | +**Residual risk:** Package names and dotfiles URLs in an imported snapshot are trusted after regex validation. The same caveats as T2 and T3 apply. |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +### T10 — Shell Theme and Plugin Injection via Snapshot Restore |
| 228 | + |
| 229 | +**Description:** The `RestoreFromSnapshot` path in `internal/shell/shell.go` writes `ZSH_THEME` and `plugins=(...)` into `~/.zshrc`. Values come from a snapshot or remote config. |
| 230 | + |
| 231 | +**Likelihood:** Low. |
| 232 | + |
| 233 | +**Impact:** Low to Medium. If `ZSH_THEME` or plugin names contain shell metacharacters, they could escape the quoted context in `.zshrc` and inject shell code that runs at login. |
| 234 | + |
| 235 | +**Mitigations:** |
| 236 | + |
| 237 | +- `validateShellIdentifier` enforces `^[a-zA-Z0-9_.-]+$` on theme names and each plugin name before they are written to `.zshrc`. This eliminates all shell metacharacters. |
| 238 | +- `buildRestoreBlock` wraps the theme in double quotes and the plugins list in parentheses, matching standard `.zshrc` syntax. The validated values cannot break out of these constructs. |
| 239 | + |
| 240 | +**Residual risk:** Negligible given the strict character allowlist. |
| 241 | + |
| 242 | +--- |
| 243 | + |
| 244 | +## 4. Intentionally Unsupported Capabilities |
| 245 | + |
| 246 | +The following were considered and deliberately excluded: |
| 247 | + |
| 248 | +**Unsigned / HTTP package sources.** `brew install` of taps from arbitrary HTTP sources is not supported. Tap names must match `owner/repo` and are resolved through Homebrew's tap infrastructure, which requires HTTPS. |
| 249 | + |
| 250 | +**`git@` SSH dotfiles URLs.** `ValidateDotfilesURL` rejects any URL that does not begin with `https://`. SSH URLs are excluded to prevent unknown host key acceptance from introducing a trust escalation vector. |
| 251 | + |
| 252 | +**Arbitrary URL in `OPENBOOT_API_URL`.** Plaintext HTTP to non-localhost is rejected. This prevents a local network attacker from trivially intercepting API traffic on misconfigured networks. |
| 253 | + |
| 254 | +**`sudo` calls in core code.** No `sudo` is invoked by OpenBoot directly. Steps that require elevated privileges (Homebrew prefix creation, Xcode CLT install) invoke their own privilege escalation flows. |
| 255 | + |
| 256 | +**`post_install` in snapshot imports.** When a local snapshot file is imported, the `post_install` field is intentionally not populated from the file. Post-install scripts only run when sourced from a live remote config with explicit user opt-in. |
| 257 | + |
| 258 | +**Unbounded goroutines.** Brew parallel installs are capped at 4 workers. This limits resource exhaustion from large package lists. |
| 259 | + |
| 260 | +--- |
| 261 | + |
| 262 | +## 5. Reporting Vulnerabilities |
| 263 | + |
| 264 | +To report a security vulnerability, email the maintainers at the address in the repository's `go.mod` module path domain (`openbootdotdev`). Do not open a public GitHub issue for vulnerabilities that could be exploited before a patch is released. |
| 265 | + |
| 266 | +Include: |
| 267 | +- A description of the vulnerability and affected code path |
| 268 | +- Steps to reproduce or a proof-of-concept |
| 269 | +- Assessed impact |
| 270 | + |
| 271 | +If the report involves an exposed credential (e.g., a hardcoded secret found in the repository), rotate the credential immediately and include the rotation confirmation in your report. |
0 commit comments