chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user