# Paul's Dashboard: security overview

A plain summary for security teams reviewing the app before approving it. It describes
how the app is built, where it keeps data, what it sends over the network, what it can do
on the machine, how updates are delivered, and how to report a problem. Deployment and the
business policy file are covered in [deployment-it.md](deployment-it.md).

Everything below describes version 3.0 and later on Windows, the only platform we ship.

## Threat model in one paragraph

The app is a personal desktop dashboard that runs as the signed-in user, with that user's
permissions, and never asks for administrator rights. We design against two things: content
the app displays (RSS feeds, web pages, AI answers, notes someone pasted, an imported
backup) trying to take over the app or the machine, and a standard, non-admin user trying to
get around the organization's policy file. Someone with local administrator rights can
change anything on the machine, including this app and its policy, and is out of scope.

## Architecture

The app is built on Electron 33 (Chromium plus Node.js).

- **Main process.** Node.js code in `src/main` owns files, settings, network calls that need
  to avoid browser CORS rules, and every OS action (opening files, launching programs,
  notifications, screen capture).
- **Renderer.** The dashboard UI (React) runs in a Chromium page with:
  - `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true` and `webSecurity: true`
    on every window, and `<webview>` disabled everywhere;
  - a preload script that exposes one object, `window.electronAPI`, made of individual
    named functions (no generic "send any message" or "read any file" call);
  - a Content-Security-Policy with no inline or remote script (`script-src 'self'`), no
    plugins (`object-src 'none'`) and no frames (`frame-src 'none'`). The small screen
    capture and color picker overlays have their own policy (`default-src 'none'`).
- **Window rules.** App windows can only show the app's own bundled page. A link, script or
  redirect that tries to move one of them anywhere else is refused, and popups are never
  created. Links open in the user's default browser, and only `http`, `https` and `mailto`
  links are handed to Windows at all, so a crafted link can't start a program through a
  `file:` path or a custom protocol handler.
- **Web permissions.** The app's own page may use the microphone (Sound Recorder), screen
  and system audio capture (Screenshots, Audio Visualizer) and the clipboard. It can't use
  the camera, location, USB, serial, HID or Bluetooth. Web pages opened inside the app (an
  optional "open in an app window" setting for links) get no permissions beyond copying text
  and fullscreen, run sandboxed with no access to `window.electronAPI`, and use their own
  separate cookie store.
- **Local file schemes.** Pictures, GIFs, note images and break videos are served to the UI
  through small custom schemes (`dash-photo:`, `gif-lib:`, `note-img:`, `dash-break:`). Each
  one only serves files from its own folder, by an ID or a strict file-name pattern, so a
  request can't climb out of that folder.

## Keeping a business policy in force

Against a standard user who wants the organization's policy gone (details in
[deployment-it.md](deployment-it.md#tamper-resistance)):

- **Enforced in the main process.** Locked settings are pinned on every write, whoever makes
  it (settings screens, widget options, imports, the command line, a crafted IPC call), and
  again at every start, so editing the data file while the app is closed changes nothing
  locked. Hidden widget types can't be created, copied, switched to or imported, and
  switched-off features are refused in the main process, not only hidden in the UI.
- **No debugger, no DevTools.** With a policy active the app won't start with
  `--remote-debugging-port`, `--remote-debugging-pipe`, `--inspect` or `--js-flags`, and
  DevTools can't open in any window.
- **Electron fuses** on the installed app: no `ELECTRON_RUN_AS_NODE`, no `NODE_OPTIONS`, no
  `--inspect`, code only from `app.asar`, `app.asar` checked against a hash in the
  executable (Windows and macOS), cookie store encrypted with DPAPI.
- **Self-check.** At startup the app checks, without changing anything, whether the person
  using it could change the policy file or folder, or the app's own install folder (a
  per-user install), and says so in Settings > About. IT should lock the policy folder and
  install with `/allusers`.

## What runs on the machine, and who decides

A few features exist to start things on the user's behalf. Each one only runs what the user
set up, and the main process checks it again rather than trusting the UI:

| Feature | What it runs | Protection |
| --- | --- | --- |
| App Launcher | A program the user added | The first run of each new or changed entry is confirmed in a native Windows dialog. Programs picked in the file picker count as confirmed. |
| Dev Servers | A shell command the user typed | The first run of each new or changed command (and folder) is confirmed in a native dialog. |
| Search Bar "custom browser" | A browser program the user chose | Same native confirmation; refusing falls back to the default browser. |
| File Shelf | Opens files the user dropped on it | Documents open normally. Programs, scripts and shortcuts (`.exe`, `.bat`, `.ps1`, `.lnk`, `.js`, `.hta`, `.msi` and similar) ask first in a native dialog. |

The native dialogs are drawn by Windows, not by the dashboard page, so nothing shown in the
dashboard can fake or click them. Confirmations are stored only on that machine; they are
never part of a backup, so a backup imported from someone else asks again before anything in
it runs. A policy can also turn whole features off (see deployment-it.md).

Main-process functions work on records the main process already holds, looked up by ID,
wherever they can. The ones that take a file path from the UI are the import and display
paths for the user's own files (adding a photo, GIF or sticker, showing an app's icon,
reading file sizes for the File Shelf, choosing where the Site Cloner saves): these only
read pictures or metadata, or write into a new folder of their own, and never run anything.

## Data storage and encryption

| Data | Where | Protection |
| --- | --- | --- |
| Screens, widgets, settings, notes, to-dos | `%APPDATA%\Paul's Dashboard\dashboard-data.json` | Plain JSON in the user's own profile folder (NTFS permissions: that user and administrators). |
| API keys, OAuth tokens, Secrets widget values, integration credentials | Same file, per field | Encrypted with Windows DPAPI through Electron `safeStorage`: they only decrypt for the same Windows user on the same machine. |
| Widget keys (CoinGecko, NewsAPI, OpenAI admin key) and Action Button webhook URLs | Moved out of the widget's plain settings into the encrypted Secrets store on launch and after an import | DPAPI, as above. The widget keeps only the entry's id. |
| Workflow secrets (credential headers, webhook step URLs, URLs with a key in the query string, the webhook trigger token) | Same file, inside each workflow | Encrypted in place with DPAPI on launch and on every save. |
| Dashboard password (optional screen lock) | Same file | Only a salted scrypt hash is kept, never the password. The lock hides the UI; it does not encrypt the data file. |
| Photos, recordings, GIFs, note images, sounds | Folders next to the data file | Plain files in the user's profile. |
| Clipboard history (Clipboard widget, off unless the widget is used; a policy can turn it off) | Data file | Plain JSON. Copies made by the Secrets and Password Generator widgets are never recorded in it. |
| Exported backups | Wherever the user saves them | Without secrets: keys, tokens, widget keys, webhook URLs, workflow secrets and recorded workflow run data left out. With secrets: a password is strongly suggested (a policy can require one), and the file is sealed with AES-256-GCM using a key derived with scrypt. A policy can also switch export or import off, or keep secrets out of every backup. |

Secrets in the UI: Secrets widget values are masked until revealed, can require the dashboard
password, and hide again after a few seconds. Copying a secret or a generated password keeps
it out of the dashboard's clipboard history and wipes it from the clipboard after a delay
(30 seconds for Secrets, 60 for the Password Generator, adjustable per widget). Windows' own
clipboard history (Win+V) and cloud clipboard, if the user has them on, keep their own copy;
Electron can't mark a clipboard entry as excluded from them. Secret values are never put in
window titles, notifications or logs. API keys go in request headers, not URLs (Google
Gemini and NewsAPI used to take them in the query string), and the logs name hosts, never
full URLs.

## Network

The app makes no network connections that the user didn't set up, apart from:

- **Update checks** to `https://paulsdashboard.com/api/updates` (see below). A policy can turn
  them off (`disabledFeatures: ["updateChecks"]`).
- **Anonymous usage analytics** (Aptabase, `us.aptabase.com`): feature-use counts and error
  messages with URLs cut down to their address, no content. **On by default**, from launch.
  Users can switch it off in Settings > About, and a policy can force it off
  (`disabledFeatures: ["analytics"]`).
- **Google Fonts** (`fonts.googleapis.com`, `fonts.gstatic.com`) for theme and widget fonts
  other than the default, Inter, which ships with the app.
- **The "Bug me" feedback form** (`https://paulsdashboard.com/api/feedback`, which passes it
  on to a Discord channel only Paul reads, or by email if that fails), only when the user sends it. A policy can turn it off
  (`disabledFeatures: ["feedback"]`).
- **`ip-api.com` over plain HTTP** for the Connections widget's country column (it sends the
  addresses this machine is talking to) and "Use my location", and
  `geocoding-api.open-meteo.com` for city search.
- **Connectivity probes** (TCP 443 to `1.1.1.1`, `8.8.8.8`, `www.microsoft.com`) when an
  uptime or status check fails.

The full list of hosts, what uses each one and what turns it off is the network map in
[deployment-it.md](deployment-it.md#network-map).

**Allow-list.** A policy's `allowedHosts` limits the app to the hosts listed. It is enforced
twice in the main process: a request filter on every Chromium session (the windows' own
fetch, images, fonts and sockets, and Electron's net module, which the updater and analytics
use), and `net-guard.js` for every request the main process makes itself, plus pings and
probes. The window's content policy still allows any `https:` host, but that doesn't matter
for the allow-list: the session filter sits below the page, so a script in a tampered page
can't reach a host off the list. Not covered: programs the dashboard starts, links opened in
the user's browser, and Chromium's DNS pre-resolution.

**Where data can go.** `backups` (export, import, required encryption, secrets in a backup),
`workflows`, `integrations`, `runPrograms` and `aiCustomEndpoints` are refused in the main
process's shared functions (the backup service used by Settings, the File menu and the
command line; the store's connection and workflow writers; the IPC handlers that start
programs). See deployment-it.md, "Data protection controls".

Everything else is something a widget the user added fetches (weather, news, RSS, stocks,
AI providers with the user's own key, and so on). A policy can turn off all internet widgets
(`networkWidgets`) or all AI (`ai`).

Server-side request protection: requests the main process makes for a URL that came from the
UI or from remote content (RSS feeds, the web/API widgets, uptime and status checks, GIF
downloads) only allow `http` and `https`, refuse loopback, private LAN, link-local
(including the cloud metadata address `169.254.169.254`), multicast and reserved addresses
in both IPv4 and IPv6 forms, and re-check every redirect. AI connections to a self-hosted
model may use a LAN address on purpose, but never link-local or metadata addresses.

A local webhook listener for Workflows binds to `127.0.0.1` only and requires each
workflow's random token.

## Updates

- The app asks `https://paulsdashboard.com/api/updates/latest.yml` (a small proxy in front of
  the project's GitHub releases) whether a newer version exists. The update is only
  downloaded when the user clicks Download, and installs on the next quit or when the user
  clicks Install.
- The download is checked against the SHA-512 hash in `latest.yml` before it is run. Both
  the file and the installer arrive over HTTPS.
- **The Windows installer is not code-signed yet.** That means an update is only as
  trustworthy as the `latest.yml` served over HTTPS by paulsdashboard.com and GitHub: if
  either were compromised, a matching malicious installer and hash could be served. Code
  signing (with the publisher name pinned in the app so only installers signed by that
  publisher are accepted) is planned. Until then, organizations that need a signature
  check should turn update checks off by policy and deploy versions they have vetted with
  their own software distribution tool.
- With `updateChecks` disabled by policy, the app makes no update requests and never
  installs an update on quit.

## Known limitations

- Electron 33 is older than the currently supported Electron releases, so it does not get
  Chromium security fixes. Upgrading Electron is planned. Until then the strict settings
  above (sandboxing, no remote code in app windows, no navigation, CSP) are what limit the
  impact of a browser engine bug.
- The installer and app are unsigned (see Updates).
- The data file is protected by Windows account permissions, not by its own encryption;
  only keys, tokens and secrets are encrypted. The person signed in can copy it; no app
  setting can stop that.
- Analytics is on by default (a user switch and a policy switch turn it off).
- `ip-api.com` is called over plain HTTP (its free tier has no HTTPS).
- The screen lock is a privacy screen for the UI, not encryption.
- A DNS name that resolves to a public address when checked and a private one when
  fetched (DNS rebinding) is only partly mitigated.

## Reporting a vulnerability

Please email **security@paulsdashboard.com** with a description, the version (Settings >
About) and steps to reproduce. Please don't open a public issue for a security problem. We
aim to reply within 3 working days and to ship a fix for serious issues as quickly as we
can, and we're happy to credit you in the release notes.
