All docs
Project
Security policy
Source: SECURITY.md on GitHub · edit this page
PulsHealth moves personal health data from an iPhone to a server the user runs. Security problems in it matter more than in most hobby projects, so please report them privately and give the maintainer a chance to fix them before anything is public.
Supported versions#
The iOS app ships from the App Store; the current version there is the supported one, and a fix reaches users in the next store release. Report against it even if you cannot build the source.
The server stack, the Swift package and the protocol tooling have no
tagged releases yet: main is the only supported line, and fixes land there.
Once tagged releases exist, this section will name the supported ones.
Reporting a vulnerability#
Use GitHub's private vulnerability reporting for this repository:
https://github.com/PulsHealth/pulshealth/security/advisories/new
That is the only reporting channel. Do not open a public issue, pull request, or discussion for a security problem, and do not email individual maintainers.
A useful report includes:
- which component is affected (iOS app,
PulsHealthSyncpackage, ingest server, product API, web viewer, Compose stack, database schema); - the commit or version you tested;
- steps or a proof of concept that reproduces the problem;
- what an attacker gains (data read, data written or deleted, denial of service, code execution, and so on);
- whether you believe it is already being exploited.
What to expect#
This is a volunteer-maintained project.
- You should get an acknowledgement within 7 days.
- You should get an initial assessment (accepted, needs more information, or not a vulnerability) within 14 days.
- Accepted reports are fixed on
mainand published as a GitHub Security Advisory that credits you, unless you ask not to be named. Until a release process exists, "fixed" means the commit is onmainand the advisory says which commit to update to. - Please allow up to 90 days before disclosing publicly. If a fix is taking longer, the maintainer will say so rather than go quiet.
Scope#
Everything in this repository is in scope, in particular:
- Ingest server (
server/ingest): the only component designed to face the network. Authentication bypass, parsing crashes, decompression or memory exhaustion, SQL injection, and anything that lets one bearer token read or modify data outside its intended reach. - Product API (
server/api): token handling, data exposure beyond the read-only role it is meant to have. - iOS app and
PulsHealthSync: handling of the server URL and bearer token, health data written outside the app container, data sent anywhere other than the configured server. - Compose stack and schema (
server/docker-compose.yml,server/db/migrations): defaults that expose a service or credential more widely than documented. - Web viewer (
web/): only as deployed the documented way — bound to loopback or a private interface. See the note below.
Out of scope:
- Vulnerabilities in upstream images and dependencies (PostgreSQL, TimescaleDB, Grafana, Next.js, Go modules). Report those upstream; a report here is welcome if the project pins a version with a known fix available.
- Deployments that diverge from the documentation, such as publishing the web viewer, Grafana, or the database port on a public interface.
- Attacks that require an unlocked phone in hand, or a compromised server host.
- HealthKit behaviour (delivery latency, permission-sheet quirks). Those are bugs, not vulnerabilities; use the issue tracker.
Things to know about the current design#
These are documented properties of the current design, tracked in
docs/open-source-plan.md. They are not vulnerabilities to report; they are
context for judging what is.
- Self-hosted. No PulsHealth service ever receives your data. Where your server runs, how it is exposed, and who can reach it are your decisions.
- Bearer tokens. The ingest server accepts two kinds. The shared
PULS_TOKENis a single static value: anyone who holds it can upload, delete, and (via the reconciliation endpoints) enumerate samples for any user, because with it theX-User-IDheader selects the user without further authentication. Per-device tokens (make devices) are stored only as a SHA-256, bound to one user — a request naming another is refused with 403 — revocable one at a time and stamped with their last use, so a lost phone costs onerevoke. The shared token stays enabled by default so an existing install is unchanged;PULS_ALLOW_SHARED_TOKEN=false(or an emptyPULS_TOKEN) turns it off, and theX-User-IDhole exists only while it is on. Failed authentications are rate-limited per client IP, which slows guessing but does not change what a leaked token grants. - The token lives on the phone. It is held in the Keychain, accessible after the first unlock so background syncs still run, and the sync-state and log files carry file protection and are excluded from device backups. If a Keychain write fails the app parks the token in that protected state file instead of dropping it — losing it would stall syncing until the user re-entered it — and removes it once the Keychain accepts it.
- TLS is yours to provide. Every service binds to loopback by default. The phone must reach the ingest port over HTTPS through a TLS-terminating reverse proxy or a VPN; the token is only a second layer.
- The web viewer has no login unless you give it one. It is a read-only
page over the health database. Setting
WEB_AUTH_PASSWORDputs it behind HTTP Basic authentication; with the variable unset it is open to anyone who can reach the port. Either way its bind address is the primary access control, so keepWEB_BIND_ADDRon loopback or a private network. - Health data at rest. The database holds identifiable data (name, email,
date of birth, sex) alongside samples. Ingest connects as the scoped
DML-only
ingestrole, which cannot create or drop objects; setINGEST_DB_USER=postgresto fall back to the superuser. Backups are opt-in and off by default: enable thebackupCompose profile, and run the restore drill inserver/README.mdyourself, because nothing else verifies that your dumps restore.