|
| 1 | +# Production Readiness Review — Dream Server CLI Installer |
| 2 | + |
| 3 | +Here is the production readiness review for the Dream Server CLI Installer based on the provided source code and test suite. |
| 4 | + |
| 5 | +## Findings |
| 6 | + |
| 7 | +### CRITICAL |
| 8 | + |
| 9 | +**1. Root Command Injection via `SUDO_USER` Variable** |
| 10 | + |
| 11 | +- **Severity**: CRITICAL |
| 12 | +- **File**: `src/lib/config.ts`, line 21 |
| 13 | +- **Category**: 1. Command Injection & Input Sanitization |
| 14 | +- **Description**: The `getUserHome()` function resolves the original user's home directory by passing the `SUDO_USER` environment variable directly into a template literal: `execSync(\`getent passwd ${sudoUser}`)`. Because `execSync` with a string argument spawns a shell (`/bin/sh`) to evaluate the command, any shell metacharacters will be executed. |
| 15 | +- **Impact**: A user or compromised script executing the installer via `sudo` can pass a maliciously crafted `SUDO_USER` variable (e.g., `SUDO_USER="root; rm -rf /"`), leading to arbitrary command execution as `root` before the CLI even processes application logic. This is a trivial Local Privilege Escalation (LPE). |
| 16 | +- **Suggested Fix**: Use `execFileSync` to pass arguments as a strict array, bypassing the shell entirely: |
| 17 | + |
| 18 | +```typescript |
| 19 | +import { execFileSync } from "node:child_process"; |
| 20 | +// ... |
| 21 | +const result = execFileSync("getent", ["passwd", sudoUser], { |
| 22 | + encoding: "utf-8", |
| 23 | + timeout: 2000, |
| 24 | +}); |
| 25 | +``` |
| 26 | + |
| 27 | +**2. Arbitrary File Overwrite / Privilege Escalation via `/tmp` (Symlink Attack)** |
| 28 | + |
| 29 | +- **Severity**: CRITICAL |
| 30 | +- **File**: `src/commands/update.ts`, lines 101-102 |
| 31 | +- **Category**: 2. Self-Update Security & 4. Concurrency & Race Conditions |
| 32 | +- **Description**: The self-updater downloads the new binary and checksum to hardcoded, world-writable paths (`/tmp/dream-installer-update`). When `curl -o` executes, it will follow any existing symlinks at that location. |
| 33 | +- **Impact**: An unprivileged local attacker can pre-create `/tmp/dream-installer-update` as a symlink pointing to a critical system file (e.g., `/etc/shadow` or `/usr/bin/sudo`). When an admin runs `sudo dream-installer update`, `curl` will overwrite the target file with the binary, resulting in complete system compromise or destruction. |
| 34 | +- **Suggested Fix**: Use `node:fs` `mkdtempSync` to create a secure, randomly named directory with strict `0700` permissions. |
| 35 | + |
| 36 | +```typescript |
| 37 | +import { mkdtempSync } from "node:fs"; |
| 38 | +import { tmpdir } from "node:os"; |
| 39 | +const tmpDir = mkdtempSync(join(tmpdir(), "dream-update-")); |
| 40 | +const tmpPath = join(tmpDir, getBinaryName()); |
| 41 | +``` |
| 42 | + |
| 43 | +### HIGH |
| 44 | + |
| 45 | +**3. Guaranteed SHA256 Verification Failure** |
| 46 | + |
| 47 | +- **Severity**: HIGH |
| 48 | +- **File**: `src/commands/update.ts`, lines 118-121 |
| 49 | +- **Category**: 2. Self-Update Security |
| 50 | +- **Description**: The downloaded `.sha256` checksum file generated by standard release tooling contains the original artifact filename (e.g., `dream-installer-linux-x64`). However, the installer saves the downloaded binary as `dream-installer-update`. When `sha256sum --check` reads the file, it searches for the original filename in `/tmp`, fails to find it, and unconditionally aborts the update. |
| 51 | +- **Impact**: The self-update integrity check is fundamentally broken and will permanently fail in production. |
| 52 | +- **Suggested Fix**: Combine this with the fix for Finding #2 by downloading the binary into the secure temporary directory using its actual artifact name (`getBinaryName()`) so `sha256sum` can successfully locate it. |
| 53 | + |
| 54 | +**4. Broken Rollback Mechanism Deletes Working Backups** |
| 55 | + |
| 56 | +- **Severity**: HIGH |
| 57 | +- **File**: `src/commands/update.ts`, lines 142-144 |
| 58 | +- **Category**: 3. Error Handling & Graceful Degradation |
| 59 | +- **Description**: After replacing the binary, the updater validates it using `await exec([currentBinary, '--version'], { throwOnError: false })`. Because `throwOnError` is explicitly false, `exec` returns the `exitCode` instead of throwing an exception if the new binary segfaults or fails. |
| 60 | +- **Impact**: The `catch` block containing the rollback logic is completely bypassed. The script incorrectly assumes success, deletes the working `.bak` backup file, and leaves the user with a permanently bricked CLI. |
| 61 | +- **Suggested Fix**: Explicitly check the `exitCode` and manually throw an error to trigger the rollback logic: |
| 62 | + |
| 63 | +```typescript |
| 64 | +const { exitCode } = await exec([currentBinary, "--version"], { |
| 65 | + throwOnError: false, |
| 66 | + timeout: 5000, |
| 67 | +}); |
| 68 | +if (exitCode !== 0) throw new Error("New binary failed execution"); |
| 69 | +``` |
| 70 | + |
| 71 | +**5. Unconditional Deletion of Named Volumes (Data Loss)** |
| 72 | + |
| 73 | +- **Severity**: HIGH |
| 74 | +- **File**: `src/commands/uninstall.ts`, lines 94-98 |
| 75 | +- **Category**: 3. Error Handling & Graceful Degradation |
| 76 | +- **Description**: During the uninstall process, the CLI executes `docker compose down -v` unconditionally. The check for the user's `--keep-data` flag doesn't occur until a later step involving directory removal. |
| 77 | +- **Impact**: Any containers relying on Docker named volumes (e.g., Qdrant, Postgres) will have their data irrevocably wiped, even if the user explicitly provided the `--keep-data` argument. |
| 78 | +- **Suggested Fix**: Conditionally append the `-v` flag to the arguments array. |
| 79 | + |
| 80 | +```typescript |
| 81 | +const downArgs = ["down"]; |
| 82 | +if (!opts.keepData) downArgs.push("-v"); |
| 83 | +await exec([...composeCmd, ...downArgs], { |
| 84 | + cwd: installDir, |
| 85 | + throwOnError: false, |
| 86 | + timeout: 30_000, |
| 87 | +}); |
| 88 | +``` |
| 89 | + |
| 90 | +**6. Catastrophic Root Directory Wipe Vector** |
| 91 | + |
| 92 | +- **Severity**: HIGH |
| 93 | +- **File**: `src/commands/uninstall.ts`, line 114 |
| 94 | +- **Category**: 1. Command Injection & Input Sanitization |
| 95 | +- **Description**: The uninstaller runs `exec(['rm', '-rf', installDir])` with no structural path validation. It relies solely on the prior detection of a `.env` file. |
| 96 | +- **Impact**: If a user runs `dream-installer uninstall --dir / --force` (or `--dir /home`) and an errant `.env` file happens to exist in that location, the script will wipe the host's root filesystem or user home directory. |
| 97 | +- **Suggested Fix**: Implement strict path safety guards against top-level system directories. |
| 98 | + |
| 99 | +```typescript |
| 100 | +import { resolve } from "node:path"; |
| 101 | +const target = resolve(installDir); |
| 102 | +if (["/", "/home", "/root", "/usr", "/etc"].includes(target)) { |
| 103 | + throw new Error( |
| 104 | + "Safety check failed: Refusing to delete critical system directory.", |
| 105 | + ); |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +### MEDIUM |
| 110 | + |
| 111 | +**7. Installer Silently Proceeds When Model Download Fails** |
| 112 | + |
| 113 | +- **Severity**: MEDIUM |
| 114 | +- **File**: `src/phases/model.ts`, lines 91-98 |
| 115 | +- **Category**: 3. Error Handling & Graceful Degradation |
| 116 | +- **Description**: If the LLM model download fails after 3 attempts, `downloadModel` simply prints a console message and returns `void` normally instead of halting execution. |
| 117 | +- **Impact**: The `install.ts` orchestrator proceeds to start the Docker Compose stack. `llama-server` immediately enters a crash-loop because the `.gguf` file is missing. The user receives a misleading "Installation Complete" message for a broken system. |
| 118 | +- **Suggested Fix**: Throw an error to halt the installation cleanly. |
| 119 | + |
| 120 | +```typescript |
| 121 | +if (!success) |
| 122 | + throw new Error("Model download failed. Cannot proceed with installation."); |
| 123 | +``` |
| 124 | + |
| 125 | +**8. Missing Context Size Updates on Tier Changes** |
| 126 | + |
| 127 | +- **Severity**: MEDIUM |
| 128 | +- **File**: `src/commands/config.ts`, lines 94-96 |
| 129 | +- **Category**: 3. Error Handling & Graceful Degradation |
| 130 | +- **Description**: When applying a new tier, the code checks `if (tierConfig.model !== currentModel)` before updating `.env`. If a user upgrades between tiers that share the same base model (e.g., upgrading Tier 1 to Tier 2 both use `qwen3-8b`), the condition evaluates to false. |
| 131 | +- **Impact**: Upgrading tiers fails to increase `CTX_SIZE` and `MAX_CONTEXT` from 16384 to 32768, depriving the user of the expected performance upgrade. |
| 132 | +- **Suggested Fix**: Always apply configuration changes if a new tier is selected, or compare the explicit tier ID instead of the model name. |
| 133 | + |
| 134 | +**9. Fallback Logic Dead Code via `throwOnError: false**` |
| 135 | + |
| 136 | +- **Severity**: MEDIUM |
| 137 | +- **File**: `src/commands/status.ts` (lines 75, 126) & `src/commands/doctor.ts` (line 195) |
| 138 | +- **Category**: 3. Error Handling & Graceful Degradation |
| 139 | +- **Description**: Multiple scripts wrap `exec(..., { throwOnError: false })` in `try...catch` blocks. Because `exec` returns an exit code object rather than throwing, the `catch` blocks are completely dead code. |
| 140 | +- **Impact**: Fallbacks (like parsing older non-JSON `docker compose ps` formats) are never triggered. In `doctor.ts`, if `nvidia-smi` crashes, it parses an empty string into `NaN < 535` (false) and logs a blank successful driver string instead of failing. |
| 141 | +- **Suggested Fix**: Remove `{ throwOnError: false }` from `exec` calls that are expected to trigger a `catch` block on failure. |
| 142 | + |
| 143 | +**10. Zero Test Coverage for Critical Execution Paths** |
| 144 | + |
| 145 | +- **Severity**: MEDIUM |
| 146 | +- **File**: `tests/update.test.ts` & `tests/doctor.test.ts` |
| 147 | +- **Category**: 7. Test Coverage Gaps |
| 148 | +- **Description**: `update.test.ts` explicitly bypasses `selfUpdate()` using `skipSelfUpdate: true`. `doctor.test.ts` never imports or executes `doctor()`, but instead manually reproduces and tests its internal regex logic. |
| 149 | +- **Impact**: The highest-risk operations (binary replacement, SHA validation, system diagnostics) have 0% effective test coverage, which allowed the critical TOCTOU and checksum bugs to remain undetected. |
| 150 | +- **Suggested Fix**: Write integration tests that invoke the actual functions, mocking out `globalThis.fetch` and `fs.mkdtempSync` for updates, and `shell.exec` for the doctor command. |
| 151 | + |
| 152 | +### LOW |
| 153 | + |
| 154 | +**11. MacOS Incompatibility for Disk Check** |
| 155 | + |
| 156 | +- **Severity**: LOW |
| 157 | +- **File**: `src/commands/doctor.ts` (line 179) & `src/phases/detection.ts` (line 58) |
| 158 | +- **Category**: 6. Docker & System Interaction |
| 159 | +- **Description**: The script executes `df -BG`. The `-B` flag is GNU-specific. On MacOS environments, this throws an "illegal option" error, bypassing the disk space check. |
| 160 | +- **Suggested Fix**: Use a POSIX-compliant format like `df -m` and perform the gigabyte division mathematically. |
| 161 | + |
| 162 | +**12. Doctor Command Hides Valid Port Conflicts** |
| 163 | + |
| 164 | +- **Severity**: LOW |
| 165 | +- **File**: `src/commands/doctor.ts`, lines 152-156 |
| 166 | +- **Category**: 8. Code Quality & Maintainability |
| 167 | +- **Description**: `doctor.ts` silently hides port conflicts if Docker is running (`&& !composeCmd`). If an external service has hijacked a port while Docker is running, the user is never warned. |
| 168 | +- **Suggested Fix**: Surface the occupied ports via `ui.info`, appending "(expected if Dream Server is running)" rather than suppressing the output entirely. |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## Summary |
| 173 | + |
| 174 | +- **Total Counts per Severity** |
| 175 | +- **CRITICAL**: 2 |
| 176 | +- **HIGH**: 4 |
| 177 | +- **MEDIUM**: 4 |
| 178 | +- **LOW**: 2 |
| 179 | + |
| 180 | +- **Top 3 Highest-Priority Fixes** |
| 181 | + |
| 182 | +1. **Fix Command Injection**: Immediately swap `execSync` for `execFileSync` in `getUserHome()` to close the `SUDO_USER` root escalation vector. |
| 183 | +2. **Rewrite Self-Update Logic**: Use `fs.mkdtempSync` to prevent symlink exploits, ensure the downloaded file matches the `.sha256` payload name, and fix the `throwOnError: false` bug so rollbacks actually work. |
| 184 | +3. **Implement Uninstall Safeguards**: Add strict path validation to prevent catastrophic system directory deletion, and conditionally append `-v` to protect Docker volumes when `--keep-data` is requested. |
| 185 | + |
| 186 | +- **Overall Production Readiness Assessment**: **NOT READY** |
| 187 | + The CLI installer has excellent orchestration logic and UX, but it ships with a root command injection vulnerability, catastrophic uninstallation data loss vectors, and a fundamentally broken self-update mechanism. Once the Critical and High severity findings are remediated, the codebase will be ready for production distribution. |
0 commit comments