# 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. - The desktop admin API may accept an RFC1918 or IPv4 link-local Host only when the TCP peer is loopback, for a trusted local reverse proxy or headless host. Do not publicly forward `/api/mobile-access`; Host and loopback checks are not a replacement for proxy access control. Mutating admin requests require a same-origin `Origin` header, except official DSH Desktop requests: its forwarder strips `Origin` and `Sec-Fetch-Site` and supplies a signed Host browser-session Cookie, so the plugin client sends an exact non-simple marker that survives forwarding. The plugin requires both that marker and Connection's successful browser-session verification before accepting an Origin-less write. ### 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. ### 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. - 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. - 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. ### 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. ## 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.