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:
- Security-pipeline operators have a single reference to cite when they need to evaluate the findings against the actual source.
- Future AI scanners can pick up the maintainer-signed claim that each flagged path is intentional, opt-in, and documented.
- 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— publicinstallCert()/uninstallCert(), per-platforminstallCertWindows/Mac/Linux.src/mitm/systemCommands.ts— sharedexecFile/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.ps1file (mode 0o600, inside a privatemkdtempSyncdirectory) and referenced via-File. The file is unlinked infinally. This removes the textbook base64-elevation-via-PowerShell fingerprint flagged by Socket.dev's AI classifier.installCertWindowscarries an inlineSECURITY-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.tssrc/lib/zed-oauth/keychain-reader.tssrc/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:
POST /api/providers/zed/discoverreturns{ candidates: [{ provider, service, account, fingerprint }] }. The raw token is never transmitted. The fingerprint issha256(service|account|token).slice(0,16).- The dashboard renders the candidate list, the operator selects which to
import, and posts
{ confirmedAccounts: [{ service, account, fingerprint }] }toPOST /api/providers/zed/import. - 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:
runElevatedPowerShellrefactor (see §1).- Inline
SECURITY-AUDITOR-NOTE:block at bothrunElevatedPowerShellandexecFileWithPassworddocuments the allowlisted callers and pinned executable list. - The
execFileWithPasswordspawn()call carries anosemgrepmarker with the allowlist of executables that the helper is allowed to receive — there is no path from user input tofinalCommand/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 persrc/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(seesrc/lib/db/migrations/071_services.sql:19). The service does not start on first launch. spawn()is called with the binary path returned byresolveSpawnArgs(apiKey, PORT)insrc/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— invokessyncKeysToCloudIfEnabled().
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:
- HMAC verification:
verifyCloudSignature(rawBody, sigHeader)checks theX-Cloud-Sigheader (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. - Secret-field opt-in:
accessToken/refreshToken/providerSpecificDataare only overwritten whenOMNIROUTE_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:
OMNIROUTE_BUILD_PROFILE=minimal npm run buildThe 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.