# Security policy `dsh-mobile` lets paired devices operate DSH and run tools with the computer user's authority. Pairing is not a read-only sandbox: trust each device and revoke it promptly if lost. Publish only the protected Mobile entry, never the ordinary DSH listener or a plaintext backend. ## Supported versions Security fixes target the newest stable release. Prereleases receive fixes only when the corresponding GitHub Release says they are supported. The README compatibility table identifies the exact DSH release tested with each plugin version. ## Reporting a vulnerability Do not open a public issue for a suspected vulnerability. Use GitHub's private vulnerability-reporting form for `saya-ch/dsh-mobile`. Include the affected version, deployment topology, reproduction steps, and whether a device credential, session Cookie, or local access is required. The maintainer will acknowledge a complete report within seven days. Publication timing is coordinated with the reporter after a fix and a revocation or upgrade path are available. ## Deployment requirements ### Listeners and desktop administration - Keep the ordinary DSH Web listener on loopback. - Expose only the plugin-owned HTTPS listener to the LAN. - By default, the desktop admin API accepts only direct loopback peers and a loopback, RFC1918 or IPv4 link-local Host. Explicit `adminAllowedCidrs` can admit additional direct peers, but each non-loopback request must also pass DSH's current request authentication; a network or source marker is not an identity. Missing authentication support fails closed. Do not publicly forward `/api/mobile-access`; proxy access control, authentication and the existing Host/Origin restrictions remain required. See [NAS administration](docs/TRUSTED_NETWORKS.en.md#accessing-the-nas-administration-page). - Mutating Web admin requests normally require a same-origin `Origin`. When a browser omits it, the plugin accepts only a JSON POST with its exact non-simple Web marker and absent or `same-origin` Fetch Metadata. Explicit foreign Origins/sites remain rejected. This marker is not a credential: its CSRF protection relies on browser CORS enforcement and the management prefix refusing OPTIONS without CORS permission. A trusted reverse proxy must not answer allowed CORS preflights or inject CORS authorization for `/api/mobile-access`. - Official DSH Desktop uses a separate path: its forwarder strips `Origin` and `Sec-Fetch-Site` and supplies a signed Host browser-session Cookie. The plugin requires both its separate Desktop marker and Connection's successful browser-session verification before accepting an Origin-less Desktop write. The Web marker cannot replace that authentication. ### Discovery, pairing and TLS - DNS-SD/mDNS, periodic UDP announcements, active UDP query replies, and HTTPS discovery return only the device name, public HTTPS origin, port, protocol version, and stable non-secret installation identifier. Discovery never returns the CA, a pairing key, a device token, Cookies, credentials, or private configuration. - For LAN pairing, only after a user selects a device and enters the fingerprint-bound pairing key may Android fetch the public CA from that exact HTTPS origin. The bootstrap GET sends no key or credential. The app retains the CA in its encrypted credential record and never adds it to Android's system trust settings. Native requests use a private trust store; WebView accepts only the otherwise-untrusted leaf signed by that CA, for the exact origin and validity period. Every other TLS error is cancelled. - Public remote origins provided by Funnel, cpolar, cloudflared, the FRP public-CA entry, or an operator-managed reverse proxy use platform-trusted HTTPS. Android stores no private CA for those credentials and cancels every TLS error. It sends a persisted device token only to the exact Origin that previously received it; a changed remote Origin requires a current one-time pairing token before the app replaces the saved credential. A QR-provided installation identifier alone never authorizes credential renewal. Browser clients likewise require a certificate trusted by their platform. - The 0.4.6 existing-frps self-signed entry instead forwards raw TCP to the computer-side HTTPS gateway. Its QR link and bare App key carry a versioned `dsh2` CA-required marker, which Android 0.4.5 and earlier reject. During explicit remote pairing, the 0.4.6 app retrieves the gateway's public CA without sending a credential, checks its validity and SHA-256 fingerprint against the pairing key, and pins it to the exact Origin; HTTP 404, a missing, changed, or untrusted CA cannot downgrade this flow to platform trust. Re-pairing an existing pinned identity retains that CA requirement. The existing exact-Origin device-token restriction still applies: a CA fingerprint alone cannot renew a device credential. The CA is not installed in Android's system trust store. The 0.4.5 app lacks this remote CA-pinning path, and ordinary phone browsers will not trust the self-signed certificate. - Keep pairing closed except during a short local onboarding action. - Revoke a lost device immediately and rotate the device registry if credential theft is suspected. ### Authentication admission and proxy peers - Invalid pairing guesses, malformed renewal/probe requests and invalid device credentials are limited by the direct TCP peer. `X-Forwarded-For`, `X-Real-IP` and `Forwarded` do not identify an authenticated client. A reverse proxy can therefore give anonymous requests a shared failure budget; this is not a reason to trust forwarded headers without an explicit proxy policy. - From plugin 0.6.12, a current valid one-time pairing secret does not consume that anonymous budget. At most one valid pairing candidate can wait for a durable mutation; it is rechecked inside the serialized operation. Renewal and native reachability probes use separate per-device budgets after credential verification, with at most one in-flight operation of each kind per device. Device capacity is independent of the anonymous peer-table capacity. Expiry and revocation are rechecked before mutation; a disconnected caller does not release work that is still pending. - These limits protect bounded application work and prevent anonymous failures from exhausting valid credentials' budgets. They do not make an exposed listener immune to connection, bandwidth or CPU exhaustion. Keep pairing windows short, protect proxy ingress, and retain the existing Host/Origin, device-token and CSRF checks. - The Gateway's internal upstream cookie exchange requires the configured loopback DSH Origin, its expected redirect status and a valid authentication Cookie. Explicit non-navigation Fetch Metadata avoids community authentication plugins' navigation HTML response; no arbitrary successful HTML page is accepted and no redirect is followed. The upstream Cookie remains server-side. ### Installation identity and device-state upgrades - Automatically identified entries without a pairing CA use the random `installation-id` file in the active Mobile state directory for new pairings, not a hash of the state-file path. Explicit operator IDs and CA-bound LAN/self-signed FRP identities remain unchanged. The identifier is public, not an authentication secret. Copying the entire state directory also copies its identity and credentials; this fix does not distinguish cloned installations. Do not delete the identity file to troubleshoot a connection. - Native pairing, renewal and authenticated reachability responses can additionally expose `installationId` for grouping independently paired LAN and remote entries from the same Mobile installation. New clients explicitly request this capability with `X-DSH-Mobile-Capabilities: installation-id`; legacy requests retain the exact prior response keys because published older Apps reject unexpected fields. This is separate from the existing channel-specific `instanceId`: CA fingerprints, legacy credential bindings and explicit operator identities retain their existing checks. The grouping identifier does not authorize a connection, replace endpoint credentials or permit sending a token to a different Origin. Discovery and QR metadata alone must not establish an automatic-switch group. Missing identifiers on older gateways disable automatic grouping; copying state can still clone the installation identity. - Initial identity creation requires a filesystem that supports hard links within the Mobile state directory. A complete private candidate is published without replacing an existing identity, so concurrent starts cannot read a partially written identifier. Unsupported filesystems or invalid identity files fail explicitly; the plugin does not silently generate a replacement. Reading an existing valid identity does not require creating a hard link. - Plugin 0.6.8 reads device-state versions 1 and 2 and writes version 2 on the next mutation. New records retain an optional `nativeInstanceId` binding, so authenticated native renewal and probe return the identity used for their pairing. Older unbound records keep the gateway's legacy identity during an ordinary upgrade with the same state path and gateway configuration; moving their state or changing their certificate/operator identity is not covered by this preservation. They are not reassigned from discovery metadata. The native response fields remain unchanged and the published Android App 0.6.3 needs no update for this fix. - Stop the old Gateway normally before updating, then restart DSH. Do not run old and new plugins against the same state concurrently. Plugin 0.6.7 and earlier cannot read version 2; if downgrade may be necessary, stop DSH and back up the complete Mobile state directory before upgrading. There is no automatic rollback or data reset. Restoring a pre-upgrade backup is a separate operator action and can also restore credentials revoked since that backup: review and revoke those devices again before exposing the entry. - Working pairings do not need to be recreated. A credential already overwritten in the App cannot be reconstructed by the plugin; pair each affected computer again after updating. A usable legacy row may remain beside a new row at the same Origin. Remove a redundant local row only after confirming the new one works; the upgrade does not automatically delete local rows or revoke their computer-side credentials. ### Remote components, diagnostics and cleanup - Do not expose the LAN gateway through router port forwarding. Funnel, cpolar, cloudflared, and FRP reach a separate loopback gateway through their selected remote path. The own reverse-proxy provider instead exposes a plugin-owned private HTTP origin restricted by its bind address and allowed source CIDRs; the operator's proxy must provide public HTTPS. Funnel, cpolar, cloudflared, Caddy, or the own proxy terminates public TLS in their respective modes; the 0.4.6 self-signed FRP entry terminates TLS on the computer. DSH pairing, device authentication, CSRF checks, and session revocation remain enforced by the plugin gateway. - Optional managed Caddy installation is available on Windows/Linux x64 from the plugin's pinned DSH Mobile component release. It uses the `tencentcloud` DNS module with operator-supplied Tencent Cloud DNSPod credentials for DNS-01, but still needs public HTTPS ingress to the computer; DNS-01 is not a NAT traversal mechanism. Expose only the intended HTTPS ingress and never expose its loopback HTTP Gateway. Component installation or the computer-side public discovery probe does not establish that a phone can reach the public route. - The Funnel node stores its Tailscale login state under `$DSH_HOME/mobile-access/remote/tailscale/`. The plugin does not request or store a Tailscale password, Auth Key, or OAuth secret. - The proxy-assisted remote diagnostic probes only the public `/mobile-access/health` path. It tries direct HTTPS first and, only after direct failure, may use an HTTP CONNECT proxy named by the DSH process environment unless `NO_PROXY` excludes the target. The probe sends no device token or session Cookie; configured proxy credentials are sent to that proxy for CONNECT, not placed in the diagnostic report. A proxy-only success is a warning to test from the phone, not proof that the phone or the provider route is available. This diagnostic does not change the remote provider's traffic path. - Plugin 0.6.11 and later record limited HTTP authorization and CSRF rejection metadata. It omits credential values and query strings, categorizes arbitrary paths, normalizes Origin and admits only recognized fetch-site values. Each Gateway keeps two fixed diagnostic categories with 30-second repeat aggregation, not an unbounded per-client/path history. Diagnostic-callback failures do not change the rejection response. These records are not complete WebSocket or native-pairing traces, and absence of a record does not establish successful authentication. Review even bounded metadata before sharing logs. - Per-Gateway HTTP diagnostics use four fixed categories: parser rejection, transport failure, invalid target, and an authenticated exact `POST /api/session/prompt` rejected upstream. Each has 30-second sampling with suppressed counts. Parser and transport codes are allowlisted; transport failures carry no HTTP status. Raw packets, headers, request bodies, error messages, arbitrary URLs and query strings are excluded. A diagnostic failure cannot change the response or retry a write. Request timeout and header overflow remain rejected, using 408 and 431 rather than a generic 400; receive limits are not relaxed. - cpolar is downloaded only after confirmation from a pinned official artifact whose size and SHA-256 are verified. Its Authtoken is stored in a private, self-update-disabled configuration under `$DSH_HOME/mobile-access/`, never returned by the admin API or written to logs. Component removal deletes its managed executable, Authtoken configuration, logs and staging files, but retains LAN and shared remote pairing records. - Self-hosted FRP accepts a VPS address, frps control port, operator-generated high-entropy shared token, and standard-port public HTTPS origin. The 0.4.6 self-signed entry additionally selects a non-reserved public TCP port on a globally routable IPv4 address. The plugin validates token length and format; the operator remains responsible for its entropy. Managed deployment creates one HTTP vhost and Caddy; existing-frps attachment changes no VPS files or services and allows either a restricted HTTP vhost with a public CA or one TCP passthrough to a computer-side HTTPS gateway. Arbitrary FRP configuration, general TCP/UDP forwarding, FRP plugins, PATH entries, and startup tasks are not supported. - For an HTTP-vhost entry, the generated managed-frps template binds the plaintext listener to `127.0.0.1` and uses Caddy for public HTTPS. An existing frps must have its actual `vhostHTTPPort` entered explicitly and its plaintext vhost kept off the public network; the plugin probes the configured port before starting `frpc`. frps `proxyBindAddr` governs proxy listeners globally: exposing a TCP passthrough on the same instance must not accidentally expose another plaintext vhost. A separate frps instance may be required. For self-signed ingress, the computer-side CA lasts five years and the leaf lasts 397 days; the leaf is renewed under the same CA, but an expired or replaced CA is not silently trusted and requires fresh pairing. A local readiness probe does not establish phone reachability; verify the public entry from an independent network. - The FRP token stays in a private file under `$DSH_HOME/mobile-access/`, is never returned by status, diagnostic, or attachment-plan responses, and is never logged. Copying a server template or an attach-mode `frpc.toml` after re-entering its token puts that token on the system clipboard; clear it after use and protect the VPS configuration. Local FRP cleanup removes the managed `frpc`, token, runtime configuration, ingress certificate material, staging files, and logs. Server-side cleanup scripts apply only to plugin-managed deployment and delete solely DSH Mobile-owned files, services, and tagged firewall rules; attach mode never deletes the operator's existing frps or Caddy files. - Automatic VPS deployment and one-click VPS cleanup first read the server's SSH host keys and require the operator to confirm every `SHA256:` fingerprint against the VPS console before any authenticated connection. All SSH/SCP channels pin `StrictHostKeyChecking=yes` to a temporary known_hosts file built from the confirmed keys; unknown, rotated, or extra keys abort the operation instead of being silently accepted. SSH credentials themselves are key-or-agent only, never passwords; the private key path is convenience state in the current browser profile, and key material is never uploaded or logged. - Disabling remote access stops the selected provider without affecting LAN or pairing records. Resetting remote access also clears the shared remote-device registry, so all remote devices must pair again. A Funnel reset additionally removes the plugin's private Tailscale node login state and requires authorization again. Other providers retain settings and components until their separate removal actions are used. ### Experimental iroh transport - iroh carries traffic over encrypted direct connections or relays to the computer's loopback Mobile Gateway. Its endpoint identity and short-lived transport-pairing code do not replace the Gateway's one-time pairing token, device authorization, CSRF checks or revocation. Transport and Gateway device authorizations remain distinct; revoking a DSH Mobile device still prevents authenticated Gateway access. - The full Android App pins the Gateway CA supplied through explicit iroh pairing and uses an App-local `*.iroh.localhost` entry. This local entry is not a public hostname or a browser-compatible tunnel. Keep its loopback traffic out of remote proxies; never bypass certificate validation to repair VPN or DNS interference. The compatibility APK and ordinary mobile browsers do not contain this native transport. - The computer component is downloaded only after confirmation, from the fixed source with pinned size and SHA-256. It runs under the current user's authority without a system service. Component removal requires iroh remote access to be disabled and deletes only the plugin-downloaded program, retaining saved identity, Gateway pairings and external companion installations. Uninstalling a binary is not revoking authorization. - Current component targets are Windows/Linux x64; the full App packages arm64-v8a and x86_64. Network reachability, direct versus relayed traffic and background delivery are not guaranteed. VPN bypass is an operator choice, not a reason to disable system security policy. See [iroh setup and troubleshooting](docs/IROH.en.md). ### Device authority, Android and extensions - Treat every paired device as a fully trusted operator. The authenticated proxy forwards ordinary DSH and community-plugin routes using GET, HEAD, POST, PUT, PATCH, or DELETE; writes require the gateway's exact Origin, valid Session, and Session-bound CSRF token. Upstream requests use DSH's loopback Host and the gateway's own DSH authentication, so a plugin cannot distinguish a paired phone from the computer user merely by checking that Host. Routes may read configuration, save settings, or run tools with the desktop user's authority; pairing is not a read-only sandbox. The separate `/api/mobile-access` admin API remains inaccessible through the mobile listener. - Android stores the paired-device list as an encrypted record. Removing a local row erases its local credential but does not revoke the phone at the computer; use the computer-side device controls to revoke access. A computer-side revocation invalidates the token even if the App retains the row's name for re-pairing. Network timeouts are not evidence of revocation. - Android grants WebView microphone access only for an audio-only request from the current paired HTTPS Origin, after Android's `RECORD_AUDIO` permission is granted. Other WebView resources and Origins remain denied. Client plugins within the authenticated page share that Origin and can invoke page APIs; install only trusted plugins before granting microphone access. - Treat `mobile.js` as application code with the paired page's same-origin authority. Restrict write access to trusted host-side DSH sessions and review generated API calls or browser-permission use. - Treat every extension `host.mjs` as a local program with the desktop user's Node.js privileges. The extension/Native Bridge API provides no source-upload or editing endpoint, but a paired DSH operator can still use ordinary DSH tools or `/mobile` to edit files. Only run trusted code. - Extension Actions and Routes receive filtered request data, a device identifier, and an abort signal; their provided request fields omit gateway credential/internal headers, and the gateway filters returned headers. These are API restrictions, not an OS sandbox preventing trusted Node.js code from accessing local data or networks. - Optional local Worker execution contains synchronous JavaScript blocking without changing that trust model. It has thread, transport and V8 limits and explicit recovery, but native crashes/all native allocations and completed side effects are not isolated or rolled back. See the [Worker guide](docs/EXTENSION_WORKERS.en.md). ### Framed content and browser policy - Proxied GUI responses allow HTTP iframe sources for compatibility with community surfaces. HTTP content is unauthenticated and unencrypted; Android still blocks mixed HTTP content inside HTTPS pages, and browsers may block it too. Prefer HTTPS and do not treat a gateway framing allowance as a transport-security guarantee. - Proxied GUI responses allow same-origin and Blob audio/video through `media-src 'self' blob:`; Gateway-owned login and pairing pages do not gain Blob media permission. External media and connections remain restricted. This permits a trusted page plugin to play a same-origin file fetched into an object URL; it does not bypass browser codec support or autoplay rules. Android retains its media user-gesture requirement. Media plugins should provide a visible poster and a user-initiated playback fallback rather than assume autoplay always succeeds. ## iPhone and iPad Web Push Web Push is opt-in per paired device, browser installation and exact HTTPS Origin. Subscription endpoints, VAPID keys and notification state are private credentials stored in the Mobile state directory; do not include them in reports or backups shared publicly. The sender encrypts generic notification text and accepts only validated Apple HTTPS endpoints, with DNS pinning, certificate verification, no redirects and bounded sends. Notification references never authorize a request, reveal conversation content, or bypass the Gateway's device authentication. Disable, logout and device revocation stop future eligible sends; already accepted notifications may still arrive. Logout must not report success while enabled consent cannot be durably disabled, including temporary loss of the optional push Host services. The explicit administrator reset affects the whole notification registry, not pairings, and is never an automatic repair. See [Web Push setup and recovery](docs/WEB_PUSH.en.md). ## Known limitation The current DSH HTML boot process contains inline JavaScript, revives Schemastery callbacks with `new Function`, and applies some styles dynamically. To keep the stock Web UI runnable, the gateway's Content Security Policy currently includes `script-src 'self' 'unsafe-inline' 'unsafe-eval'` and `style-src 'self' 'unsafe-inline'`. The remaining directives still restrict origins, connections, frames, objects, workers, images, and form targets, but this policy does not eliminate script-injection risk. Removing these allowances requires upstream DSH support for nonces, stable hashes, external boot resources, and a non-evaluating schema representation. Do not include live private keys, npm tokens, pairing values, device credentials, Cookies or captured private settings in issues, logs, committed fixtures or examples. Authentication tests use isolated temporary credentials, not real user state.