# Security Policy Manef Shell OS is Public Alpha / Developer Preview software. It has not had a third-party security audit. Only the latest commit on `main` is supported; there are no release branches yet. The project's repeatable automated and manual verification lanes are documented in [`docs/SECURITY-ASSURANCE.md`](./docs/SECURITY-ASSURANCE.md). They are security evidence, not a certification or a substitute for an independent third-party audit. ## Reporting a vulnerability Do not open a public issue containing exploit details or secrets. GitHub private vulnerability reporting is enabled for this repository; use [**Report a vulnerability**](https://github.com/rahmanef63/mso/security/advisories/new). Include the affected commit, reproduction steps, impact and sanitized logs. Never post passwords, session secrets, API keys, bearer tokens, private file contents, Camoufox profile data or full environment files. ## Deployment warning An authenticated MSO **Owner** session can read allowed files and run commands as the Linux user that owns the process. Treat Owner like SSH in a browser. Viewer and Operator are narrower application roles, but they still share the same deployment and underlying Unix account. - Run MSO as a dedicated non-root user. - Prefer Tailscale/VPN for a real deployment. - Otherwise use HTTPS plus firewall/allowlist controls in front of the app. - Do not expose the raw app port directly to the public internet. On laptop/WSL installs, `mso gateway start` keeps MSO on loopback and creates an outbound HTTPS tunnel instead. The temporary no-domain mode is preview-only; use a named tunnel/TLS reverse proxy plus an upstream access policy for a permanent Internet-facing control plane. - Keep `OS_FS_WRITE_ROOTS` narrow and review read roots deliberately. - Do not commit `.env.local`, model credentials or data under `~/.mso`. - Gateway/update lifecycle state is owner-only and fail-closed: public tunneling requires verified loopback-only exposure; offline recovery uses checkout-scoped receipts, an exclusive process-identity lock, and durable pre-quiesce recovery intent rather than treating Git HEAD as deployment proof. Tunnel and fallback-runtime startup additionally use a parent/child release handshake, so neither Cloudflared nor Next can execute before its process lifetime is tracked; interruption before release makes the held child self-exit. Gateway state is namespaced by canonical checkout + loopback origin, lifecycle/update serialization uses crash-released kernel `flock`; a checkout-wide runtime exclusion prevents `mso web`/gateway runtime starts during any in-place `.next` mutation; service-active and offline updates inventory/quiesce every checkout-owned fallback runtime, and a live tunnel with a dead local runtime is reconciled before reuse. Public readiness must match the exact local build/runtime identity rather than only the MSO version. - Use `NEXT_PUBLIC_OS_DEMO=1` only in a separate mock-only public demo checkout. The core gateway client follows the same immutable-artifact rule as managed apps: MSO pins the official `cloudflared` release URL and SHA-256 per supported Linux architecture in `security/gateway-artifacts.env`, stores it user-locally, re-hashes before execution, and disables Cloudflare self-update. `MSO_GATEWAY_CLOUDFLARED` is an explicit operator override, not an automatic PATH fallback. The tunnel child starts from a scrubbed environment (HOME/PATH/LANG only); MSO login, session, provider and API-key environment variables stay with the application runtime and are not inherited by `cloudflared`. ## Authentication and sessions The deployment login is password + device approval. A correct password on a new browser creates a pending device; an Owner device or the local server CLI assigns it one live role before a normal session is issued: - **Viewer** — read-scoped workspace surfaces such as bounded files, telemetry, service inventory, package visibility, docs and previews; - **Operator** — Viewer plus bounded operational surfaces such as Camoufox, managed-app start/stop/restart/backup, service journals and exact-allowlisted service actions; - **Owner** — full MSO host authority, including writes, PTY/exec, credentials, self-update, MCP, device administration and managed-app install/update. Roles are read from the private device store on every request; demotion and revocation therefore do not wait for the signed cookie to expire. Shared Appearance, Theme and Quicklink preferences may be read by delegated devices but changed only by Owner because the current store is deployment-wide, not per-device. The UI also filters inaccessible apps, but the route policy is the actual boundary: unknown mutations fail up to Owner by default. Role-less records created by older MSO versions migrate as Owner; malformed role values fail down to Viewer. Changing `OS_SESSION_SECRET` invalidates existing sessions. Removing a device from the allowlist revokes that browser. Device approval is an allowlist, not standards-based MFA, a named-user directory, Linux-account mapping, or enterprise SSO. Multiple people can use distinct approved devices, but they still authenticate to one deployment secret and one Unix service account. ## Filesystem and command boundary File routes inherit `OS_FS_READ_ROOTS` / `OS_FS_WRITE_ROOTS`, canonical containment checks and a credential denylist. MSO's own private state, `.env*`, SSH/GPG material and other sensitive-home paths are hidden/refused unless the supervised sensitive-path escape hatch is explicitly enabled. Linux/WSL file reads validate the opened descriptor; raw downloads stream that same handle. Mutations and recursive walks hold directory descriptors, so exchanged ancestor paths cannot redirect an authorized operation. Uploads retain their directory handle until commit; ZIP reads build a bounded private snapshot before the external archiver runs. Configure resolved absolute roots: a root replaced with a symlink does not grant access to its new target. These guards fail closed without Linux `/proc` descriptor support; official macOS/Termux installs use a Linux guest. Workflow graphs recheck the live device before every action. Operator graphs can read and perform only reviewed managed-app start/stop/restart/backup actions; generic writes, installation, provider configuration and new write capabilities require Owner. The tool directory follows the same policy. Inbound A2A clamps token scope to the current deployment ceiling at dispatch, each model round and each tool invocation. Local-agent message streams retain the signed session and stop before another delivery or within one second while idle after revocation, demotion, logout, expiry or cookie-policy rotation. Managed-app cookies use host-only `__Host-` names, `Secure` and `Path=/` on HTTPS app origins. Legacy and duplicate names are refused, so existing app logins may need to be renewed after an update. OAuth discovery ignores forwarded authority, prefers the configured public origin and uses `no-store`. Fresh installation fetches and verifies the requested Git ref and fails instead of using the default branch. `exec.run` / MCP `exec_run` runs as the MSO Linux user. The destructive-command matcher is a short accident tripwire, not a sandbox. Interactive Terminal PTYs are even more direct: raw keystrokes cannot be reliably parsed into commands, so authentication and PTY session lifecycle—not the one-shot command matcher—are the boundary. ## Static-analysis and file-safety boundaries Hosted CodeQL findings are treated as release blockers until they are either remediated or proven false positives with a narrow, reviewable justification. The current implementation applies the following source-level controls in addition to route authorization: - provider/config/OAuth maps accept validated provider identifiers and rebuild records from entry lists rather than assigning or deleting attacker-selected object properties; - login-secret equality uses a fixed-width constant-time byte comparison; no fast password hash or reusable password verifier is created, and over-limit UTF-8 input fails closed; - thread identifiers are accepted or rejected as a whole, then pass an explicit `path.relative` containment check whose filesystem sink stays inside the proven-safe branch before any filesystem operation; they are never rewritten into colliding names; - bounded host reads, lock recovery, skill/catalog reads and model-cache reads bind metadata and content to the same `O_NOFOLLOW` file descriptor where the platform supports it; - read/write root policy performs lexical containment before filesystem resolution and canonical containment afterward, so nonexistent outside paths and escaping symlinks both fail closed; - the models.dev catalog uses one fixed HTTPS endpoint, bounded streaming, object-shape validation, a private `0700` cache directory and atomic `0600` cache replacement; - external widget embeds allow only credential-free external HTTPS URLs, omit `allow-same-origin`, and send no referrer; local image previews accept a raster MIME allowlist and revoke object URLs; - uploaded files use random exclusive temporary names, revalidate the destination after directory creation, reject traversal instead of normalizing it away, and remove partial files on every exit; - the intentional ChatGPT-to-VPS file bridge is write-scope only, accepts temporary URLs only from reviewed OpenAI content/storage hosts, revalidates every redirect, streams with a 20 MiB hard cap, checks response MIME and PNG/JPEG/WebP signatures, then enters the same write-root, credential, exclusive-temporary-file and atomic-rename path as an ordinary authenticated upload. Generic `application/octet-stream` remains deliberately content-agnostic because this is a file-transfer surface, not an executable installer; transferred bytes are never executed automatically by MSO. These controls reduce known classes of path race, prototype-pollution, DOM trust, and unbounded network-to-disk issues. CodeQL can still correctly describe the last bridge as network data written to disk: that data flow is the feature itself, so any GitHub dismissal must use the narrow `won't fix` classification with this control rationale—not `false positive`. None of these controls turn automated static analysis into a proof of complete security. ## Service and package operations System Monitor exposes system/user `systemd` inventory to Viewer devices. Journal reads require Operator, are capped, and accept only validated `.service` unit names. Lifecycle actions require Operator or Owner **and** an exact `scope:unit` entry in `OS_SERVICE_CONTROL_UNITS`; empty is the default, wildcards and malformed entries are rejected, and the host helper uses fixed argv without a shell. System-scope control additionally depends on the Unix user's existing permission, or an explicit `OS_SERVICE_CONTROL_USE_SUDO=1` deployment with narrowly-scoped non-interactive sudo. Package Updates is visibility-only. It reads the supported package manager's existing local cache with bounded commands (`apt list --upgradable`, cache-only DNF/YUM, `pacman -Qu`, or no-refresh Zypper). It does not refresh metadata, install packages, or perform an upgrade. ## Alfa model data Alfa credentials are stored server-side in private host config. BYOK means the owner controls the credential; it does **not** mean model traffic stays on the VPS. Messages and tool context included in a model request go to the selected provider. Alfa's read tools can supply file/process/log data to the model. Host mutations use the visible Alfa approval-card boundary. Treat file/log contents as untrusted prompt input and review the actual proposed mutation, not only the model's explanation. ## MCP / ChatGPT / external AI clients The MCP server is off unless `OS_MCP_ENABLED=1`. External clients authenticate through MSO's OAuth 2.1 + PKCE flow and receive a bearer with `read`, `write` or `exec` scope, capped by `OS_MCP_MAX_SCOPE`. An MCP bearer is a standing credential. At `exec` scope it can execute host commands as the MSO user. Scope is rechecked on every tool call. Tool inputs and results are also processed by the connected AI client/provider according to that product's data controls. Use `read` unless a workflow truly needs more, revoke stale clients in Settings → MCP and refresh the client tool snapshot when MSO's toolset signature changes. See [`docs/MCP.md`](./docs/MCP.md) and [`docs/CHATGPT-PLUGIN.md`](./docs/CHATGPT-PLUGIN.md). `browser_status` deliberately never returns Camoufox's viewer password or profile data. `screen_capture` is limited to MSO itself, not arbitrary web pages. ## Managed applications and per-app origins MSO can manage Hermes, OpenClaw and 9Router as separate applications. They keep their own runtime, config, state and host privileges. MSO can install them, control lifecycle, read logs, update, back up, restore and conservatively uninstall through explicit managed-app paths. Restore is the intentional exception to the general rule that MSO should not edit another app's state: it is heavily gated, requires the app stopped, creates a pre-restore safety snapshot and writes only into the verified state directory. Vendor dashboards require `allow-same-origin` to function. Running that JavaScript on the cockpit origin would give it the owner's browser realm/session. MSO therefore has no supported same-origin dashboard mode. The safe default is **no embedded vendor dashboard**. To embed dashboards, set `NEXT_PUBLIC_MANAGED_APP_HOST_TEMPLATE` and give each app a separate hostname, for example: ```text mso.example.com hermes.mso.example.com openclaw.mso.example.com ``` On a managed-app hostname every request is routed only into that app's proxy and cockpit pages/API/static chunks are not served there. Keep DNS explicit; do not point arbitrary wildcards inside the shared cookie domain at MSO. Camoufox/noVNC uses the same browser-realm principle through the reserved `camoufox` host in that namespace. The host accepts only Operator/Owner-session GET/HEAD requests, strips cockpit cookies/authorization before loopback noVNC, and applies a restrictive CSP. The old `/camoufox-vnc/*` cockpit path is permanently closed. Revoking an approved device also stops the Camoufox service cgroup so an already-established VNC WebSocket is evicted. Managed-app installation is also fail-closed. Hermes installer bytes and checkout commit, OpenClaw package bytes/version, and the 9Router image digest are committed in `security/managed-app-artifacts.env` and verified before execution. 9Router binds to loopback by default; `NINE_ROUTER_EXPOSE_PUBLIC=1` is an explicit operator exception, not an inferred fallback. This is a browser-realm boundary only. A plugin installed **inside Hermes or OpenClaw** runs with that daemon's host privileges. Installing an untrusted daemon plugin is equivalent to trusting code with that user's capabilities; iframe origin separation does not sandbox it. ## Managed-app backups Managed-app snapshots live under `~/.mso/backups///` and can contain the application's own credentials, pairings and history. Treat those copies like the source state. They are private but not encrypted by MSO. Backups skip symlinks and reinstallable/cache directories. Restore validates the manifest, app identity, state path and symlink collisions before writing, then creates an additional `pre-restore` safety snapshot. See [`docs/MANAGED-APPS.md`](./docs/MANAGED-APPS.md). ## In scope Authenticated raw-file responses allow four concurrent streams per device and sixteen per process. Each response is capped at 32 MiB; larger files require bounded byte ranges. Streams close after ten idle seconds or sixty total seconds, and cancellation closes the pinned descriptor. Saturation returns HTTP 429 before opening the file. - auth/session/device-approval or role-escalation bypass; - practical login rate-limit defeat; - filesystem-jail or credential-denylist escape; - unauthorized access to live host/config routes or a role-restricted app; - service-control allowlist bypass, unit/argv injection, or package-visibility mutation; - CSRF/clickjacking that triggers owner host actions; - MCP scope/OAuth bypass or bearer validation failure; - managed-app origin escape into the cockpit or a sibling app; - managed-app proxy SSRF/target escape; - backup/restore path escape or symlink-following writes; - secret leakage through logs, tool descriptors, temporary shares or browser-status APIs. ## Out of scope - an already-authenticated Owner intentionally using documented Owner capabilities; - an Operator intentionally controlling an exact owner-allowlisted service; - bypassing the one-shot destructive-command regex after the user already granted shell execution—it is not presented as a sandbox; - deployments that ignore the minimum documented posture; - what trusted/untrusted code installed *inside* Hermes/OpenClaw can do with that daemon's own host permissions; - prompt/model quality issues that do not bypass MSO's server-side permission boundary. ## Rotation and retention - change `OS_SESSION_SECRET` to invalidate browser sessions; - change `OS_LOGIN_PASSWORD`, or use Settings → Account → Reset password, to rotate the owner password; - change device roles or revoke browsers in Settings → Account → Devices / `mso device`; use the local CLI for recovery; - remove/revoke MCP tokens from Settings → MCP; - rotate provider credentials from Settings → AI or the corresponding environment source; - rotate managed-app credentials if a state snapshot containing them was exposed. `~/.mso/audit.log` is append-oriented local forensic data, rotated at 8 MiB. Inspect it before sharing because paths and command context can be sensitive.