File size: 11,119 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
---
title: "Socket.dev Supply-Chain Finding Attestation"
description: "Maintainer attestation for the AI-detected potential-malware findings raised against omniroute and the v3.8.6 mitigations applied at each flagged call site."
---

# Socket.dev / supply-chain finding attestation

This document is the maintainer-authored attestation for the six
`AI-detected potential malware` findings raised against `omniroute@3.8.5` and
the mitigations applied in `omniroute@3.8.6`. It exists so:

1. Security-pipeline operators have a single reference to cite when they need
   to evaluate the findings against the actual source.
2. Future AI scanners can pick up the maintainer-signed claim that each
   flagged path is intentional, opt-in, and documented.
3. We have a written record of *why* each call site is shaped the way it is β€”
   so a future refactor doesn't accidentally reintroduce a fingerprint that
   was deliberately removed.

If you operate a scanner that re-flags any of the call sites below after the
v3.8.6 mitigations have shipped, please open an issue with the scan trace and
we will extend the attestation here.

---

## Β§1 β€” MITM root-CA install (`77484.js`)

**Source files**:

- `src/mitm/cert/install.ts` β€” public `installCert()` / `uninstallCert()`,
  per-platform `installCertWindows/Mac/Linux`.
- `src/mitm/systemCommands.ts` β€” shared `execFile` / `spawn` / PowerShell
  helpers used by the install paths.

**Trigger**: user clicks "Enable MITM proxy" in the local dashboard at
`/dashboard/cli-tools/mitm`. The route is loopback-only β€” see hard rule #17 in
`CLAUDE.md` and `src/server/authz/routeGuard.ts::isLocalOnlyPath()`. A leaked
JWT exposed via a tunnel **cannot** trigger this code path.

**Privileged operations performed (per platform)**:

| OS      | Command(s)                                                                                     |
| ------- | ---------------------------------------------------------------------------------------------- |
| Windows | `certutil -addstore Root <cert>` via UAC                                                       |
| macOS   | `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <cert>`  |
| Linux   | `sudo cp <cert> <distro-trust-dir>` + `sudo update-ca-certificates` (Debian) / `sudo update-ca-trust` (RHEL/SUSE) |
| Linux+Firefox/Chromium | per-profile NSS DB update via `certutil -d sql:<profile>`                          |

These are the same commands used by `mitmproxy`, Charles Proxy, Fiddler, and
Caddy. The fact that they exist in OmniRoute is documented at
`docs/security/STEALTH_GUIDE.md`.

**v3.8.6 mitigation**:

- `runElevatedPowerShell()` no longer uses `-EncodedCommand <base64utf16le>`.
  The elevated payload is written to a per-call temp `.ps1` file (mode 0o600,
  inside a private `mkdtempSync` directory) and referenced via `-File`. The
  file is unlinked in `finally`. This removes the textbook
  base64-elevation-via-PowerShell fingerprint flagged by Socket.dev's AI
  classifier.
- `installCertWindows` carries an inline `SECURITY-AUDITOR-NOTE:` block
  pointing here.

**Why we keep it**: the MITM proxy is a documented feature used by
`docs/security/STEALTH_GUIDE.md` and `docs/frameworks/MITM-PROXY.md`. Removing
it would break the agent-bridge feature set.

---

## Β§2 β€” Zed credential import (`app/api/providers/zed/import/route.js`)

**Source files**:

- `src/app/api/providers/zed/discover/route.ts` *(new in v3.8.6)*
- `src/app/api/providers/zed/import/route.ts`
- `src/lib/zed-oauth/keychain-reader.ts`
- `src/lib/zed-oauth/credentialFingerprint.ts` *(new in v3.8.6)*

**Trigger**: user clicks "Import from Zed" in the local dashboard Providers
page. Endpoint is gated by `requireManagementAuth`. The Zed editor itself
writes its provider API keys to the OS keychain under documented service
names β€” see https://zed.dev/docs/ai/llm-providers.

**v3.8.5 behaviour (the one Socket.dev flagged)**:

`POST /import` discovered the credentials and auto-saved them to the local
SQLite store in a single round-trip. No per-account confirmation, no
fingerprint, just "found N tokens, all imported."

**v3.8.6 mitigation β€” 2-step confirmation**:

1. **`POST /api/providers/zed/discover`** returns
   `{ candidates: [{ provider, service, account, fingerprint }] }`. The raw
   token is **never** transmitted. The fingerprint is
   `sha256(service|account|token).slice(0,16)`.
2. The dashboard renders the candidate list, the operator selects which to
   import, and posts `{ confirmedAccounts: [{ service, account, fingerprint }] }`
   to **`POST /api/providers/zed/import`**.
3. The import endpoint **re-reads the keychain on the server** and filters by
   `(service, account, fingerprint)`. A tampered or replayed discover
   response cannot trick the import endpoint into saving an unrelated token β€”
   if the live token has changed since discover, the fingerprint no longer
   matches and the credential is skipped.

A `OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP=true` env flag preserves the v3.8.5
behaviour for operators who haven't yet updated their automation. It will be
removed in v3.9.

**Why we keep it**: Zed import is the friendliest onboarding path for users
who already use Zed and want to mirror their provider keys into OmniRoute
without re-pasting.

---

## Β§3 β€” `execFile` / `spawn` / elevated PowerShell (`21843.js`)

**Source files**: `src/mitm/systemCommands.ts`.

**Why flagged**: the chunk re-exports `execFileWithPassword`,
`runElevatedPowerShell`, and the shared `quotePowerShell` helper. Socket.dev's
AI classifier sees them as a generic "host execution + privilege elevation
toolkit." Within OmniRoute they are only used by the MITM cert install path
(Β§1) and by `execFileWithPassword` for `sudo` command execution.

**v3.8.6 mitigation**:

- `runElevatedPowerShell` refactor (see Β§1).
- Inline `SECURITY-AUDITOR-NOTE:` block at both
  `runElevatedPowerShell` and `execFileWithPassword` documents the allowlisted
  callers and pinned executable list.
- The `execFileWithPassword` `spawn()` call carries a `nosemgrep` marker with
  the allowlist of executables that the helper is allowed to receive β€” there
  is **no path from user input to `finalCommand`/`finalArgs`**.

---

## Β§4 / Β§6 β€” 9router service supervisor (`api/services/9router/{start,restart}/route.js`)

**Source files**:

- `src/app/api/services/9router/_lib.ts` β€” supervisor factory.
- `src/app/api/services/9router/{start,stop,restart,status,install,update,auto-start}/route.ts`.
- `src/lib/services/ServiceSupervisor.ts` β€” generic spawn / health-poll / log-buffer.

**Trigger**: user clicks "Install" / "Start" on the embedded services page in
the local dashboard.

**Already-in-place protections**:

- All `/api/services/*` routes are LOCAL_ONLY per
  `src/server/authz/routeGuard.ts` (hard rule #17). Loopback enforcement
  happens before any auth check β€” a leaked JWT cannot reach them.
- The 9router DB row is seeded as `status='not_installed', auto_start=0` (see
  `src/lib/db/migrations/071_services.sql:19`). The service does **not** start
  on first launch.
- `spawn()` is called with the binary path returned by
  `resolveSpawnArgs(apiKey, PORT)` in `src/lib/services/installers/ninerouter.ts`,
  which is a fixed allowlist of supported binaries.
- Stdout/stderr is buffered in memory (5 MB cap, see `_lib.ts`) β€” no on-disk
  write unless the user enables logging from the dashboard.

**v3.8.6 mitigation**: no functional change. The minimal build profile
(`OMNIROUTE_BUILD_PROFILE=minimal`) replaces
`src/lib/services/installers/ninerouter.ts` with a stub for users who want
the privileged paths physically removed from the bundle.

**Why we keep it**: 9router is an optional locally-installable companion
service (think: WordPress-style plugin) β€” strict opt-in.

---

## Β§5 β€” OmniRoute Cloud Sync credential write-back (`api/keys/[id]/route.js`)

**Source files**:

- `src/lib/cloudSync.ts` β€” `syncToCloud()` / `updateLocalTokens()`.
- `src/app/api/keys/[id]/route.ts` β€” invokes `syncKeysToCloudIfEnabled()`.

**Trigger**: `isCloudEnabled()` returns `true` (set from the dashboard) **and**
`CLOUD_URL` is configured. With both off, no outbound network call to the
Cloud endpoint is made.

**v3.8.5 behaviour (the bug Socket.dev caught the right way)**:

`updateLocalTokens()` overwrote `accessToken`, `refreshToken`, and
`providerSpecificData` from the Cloud response when
`cloudUpdatedAt > localUpdatedAt`. No HMAC, no signature, no checksum. A
misconfigured or hostile `CLOUD_URL` (or a MITM on the channel) could swap
provider OAuth tokens silently.

**v3.8.6 mitigation**:

1. **HMAC verification**: `verifyCloudSignature(rawBody, sigHeader)` checks
   the `X-Cloud-Sig` header (`HMAC-SHA256(OMNIROUTE_CLOUD_SYNC_SECRET,
   rawBody)`) before parsing the JSON. If the secret is set, the signature is
   required. If not (legacy mode), a warning is logged and the response is
   accepted β€” the secret will be required in v3.9.
2. **Secret-field opt-in**: `accessToken` / `refreshToken` /
   `providerSpecificData` are **only** overwritten when
   `OMNIROUTE_CLOUD_SYNC_SECRETS=true`. The default mode syncs only
   non-credential metadata (`expiresAt`, `status`, `lastError*`,
   `rateLimitedUntil`, `updatedAt`). This is a **breaking change** for users
   who relied on remote token sync β€” they must explicitly opt in.

**Why we keep it**: Cloud Sync is the only way for an OmniRoute Cloud tenant
to centralise team credentials. The fix makes the threat model honest:
"server signs, client verifies, operator opts in."

---

## Build profile: `minimal`

For users who need a Socket-friendly artifact, build with:

```bash
OMNIROUTE_BUILD_PROFILE=minimal npm run build
```

The webpack `NormalModuleReplacementPlugin` aliases four modules to stubs:

| Module                                              | Stub                                                         |
| --------------------------------------------------- | ------------------------------------------------------------ |
| `src/mitm/cert/install.ts`                          | `src/mitm/cert/install.stub.ts`                              |
| `src/lib/zed-oauth/keychain-reader.ts`              | `src/lib/zed-oauth/keychain-reader.stub.ts`                  |
| `src/lib/cloudSync.ts`                              | `src/lib/cloudSync.stub.ts`                                  |
| `src/lib/services/installers/ninerouter.ts`         | `src/lib/services/installers/ninerouter.stub.ts`             |

Each stub exports the same surface but every function throws a
`featureDisabledError(name)` at runtime. Routes that depend on the disabled
module return HTTP 503 with a clear message instead of activating the
sensitive code path.

The resulting bundle is intended to be published as `omniroute-secure`. See
`docs/ops/PUBLISHING_SECURE.md` for the publishing recipe.

---

## Plugin split (tracked for v4)

Long-term, we intend to split the npm package into separately auditable
modules. See the v4 milestone in the GitHub issue tracker for the tracking
issue.