mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* docs(b2-7): trust model — who can read what (BPR-050/051, C-09 contract)
One document states the operator trust model in plain language and traces
every claim to a code line or test: server-readable text and files and why,
E2EE voice/video/screen with the key-holder and TOFU rules, transport per TLS
mode with desktop pinning and the browser rule, the C-09 preview destination
contract B7 implements, at-rest storage, operator can/cannot, multi-device
sessions, and what beta does not claim. Linked from security.md,
deployment.md, quick-start.md and docs/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* test(b2-7): absence proofs — no federation/directory routes, outbound host table
TestAbsenceContract_NoFederationDirectoryOrListingRoutes builds the production
router with uploads, voice and GIF on, walks the whole mounted tree with
chi.Walk and fails on any route matching federat|directory|discover|listing.
A floor on the route count and a check that the admin subtree was traversed
keep it from passing vacuously. trust-model.md gains "What OwnCord does not
have" (BPR-040/082/083) and the outbound-host table B6's network capture
checks against.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* docs(b2-7): plugin boundary — off twice, compiled out of releases, no API promise
docs/architecture/plugins.md records the experimental WASM boundary (BPR-080/
081, BG-17): disabled by build tag and by config, absent from release.yml and
Dockerfile builds, the HP-2 configuration audit (fresh, upgraded, Docker,
standalone), the beta release-notes wording, what exists today with its limits
and tests, the post-beta plugin candidates that stay in core during beta, and
the core concerns that never move. Linked from architecture/README.md,
architecture/server.md and docs/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* docs(b2-7): L-08 re-tagged to B10 with the reason; B2-7 evidence block
L-08's remaining gate ("deterministic source build passes") cannot pass in
principle — TinyGo embeds host paths and has no -trimpath — and a compile-only
drift check would need a second Go SDK, TinyGo and Binaryen on every PR for a
subsystem release builds compile out. Re-tagged to B10, which runs the compile
once against the release candidate or closes on the provenance record; the
"no API promise" half is closed by docs/architecture/plugins.md. The plan's
B2-7 evidence block records the four pre-squash SHAs, the absence test's RED
output, the release-build finding, the decision, and the BPR-051 reader
placeholder. CHANGELOG gains a Documentation block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex review — TOFU windows disclosed, legacy TOTP, absence test at three boundaries
Two P1 and three P2 from the Codex review of 56f23a36, all verified against
the code. trust-model.md now states the desktop's first-connection TLS TOFU
window and the out-of-band fingerprint check; scopes identity-key pinning to
changes after the first pin, not first contact; discloses that databases from
before TOTP encryption may still hold plaintext secrets and how re-enrolment
fixes that. The absence contract gains two sibling tests — WebSocket wire
types from protocol/schema.json and every koanf key of config.Config (one
allowlisted on-disk path whose presence the test asserts) — and the document
states what the three tests bound. plugins.md corrected: unknown config keys
are warned about and ignored, not rejected. Evidence block records PR #1443
and the review outcome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex re-review — desktop first-use window in every TLS mode; tls off is plaintext
The desktop pins the fingerprint it sees on first connection in every
tls.mode (CaptureVerifier, no web-PKI validation on the server connection),
so a public-CA certificate closes the first-use window only for a browser;
the short answer and the pinning list now say so. tls.mode off served
directly is plaintext HTTP with nothing enforcing a proxy; the transport
table row states it. Evidence block records the round-2 outcome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 3 — E2EE does not survive a hostile operator on first contact; memory cap is server-wide
Identity keys are trusted on first use, so a modified server can deliver an
unpinned peer's first announce with keys the operator holds and the key
holder wraps the room key to it. trust-model.md now scopes E2EE to an
operator who reads, and to a modified server only for peers pinned and
compared out of band beforehand, in every place the stronger claim stood.
plugins.md: the wazero runtime is sized from plugins.max_memory_mb alone;
a manifest's memory value is validated but not applied. Evidence block
records the round.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 4 — transport intro excludes tls off; ipAllowed is not the full C-09 deny-set
The transport section opened with "everything is TLS"; it now excludes
tls.mode off. The C-09 contract cited the server's ipAllowed as the complete
address deny-set; it rejects loopback, private, link-local, unspecified,
multicast and CGN only, so the clause now lists the documentation and
benchmarking ranges the native broker must add and records that widening
ipAllowed is a separate server change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 5 — backups exclude uploads; release-time appimagetool fetch listed
The built-in backup is VACUUM INTO of the SQLite file only; the trust model
and deployment.md now say uploaded files are not in it and upload.storage_dir
needs its own backup. The outbound-host section scopes "no script fetches an
external host" to scripts the server runs and lists the release workflow's
build-time appimagetool download.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
* fix(b2-7): Codex round 6 — one pin per account, LiveKit media in the host table, quick-start wording
Identity pins are keyed by host and user id, so a peer's second device
overwrites the pin and the first device then mismatches; the document no
longer claims per-device pinning. The outbound-host table gains the
supervised LiveKit subprocess's WebRTC media and scopes the capture contract
to traffic the server initiates. The quick-start cross-link no longer says
voice and video are unreadable by the operator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg9QQWVN3E5UUgBD2dydtu
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
141 lines
9.1 KiB
Markdown
141 lines
9.1 KiB
Markdown
# Security Policy
|
||
|
||
Security guidelines and vulnerability reporting for OwnCord.
|
||
|
||
## Reporting Vulnerabilities
|
||
|
||
Report privately via GitHub Security Advisories:
|
||
[github.com/J3vb/OwnCord/security/advisories/new](https://github.com/J3vb/OwnCord/security/advisories/new).
|
||
|
||
**Do NOT open public issues for security bugs.**
|
||
|
||
The repository-root [SECURITY.md](../SECURITY.md) is the canonical reporting
|
||
policy — what to include and the response timeline (initial response within
|
||
7 days) live there, so the two files cannot disagree.
|
||
|
||
## What stays private, and for how long
|
||
|
||
This applies to weaknesses in the repository's own automation and settings —
|
||
workflow authorization, credential scope, release gating — as much as to bugs in
|
||
the server or client. Planning documents cite this section as the rule; it is
|
||
written here so the citation points at something.
|
||
|
||
- Public artifacts — commits, issues, pull request descriptions, changelogs —
|
||
carry an **opaque identifier, the affected property, safe acceptance criteria,
|
||
and a status**. Nothing more.
|
||
- Reproduction steps, source-to-sink traces, exploit conditions, and the state a
|
||
fix replaced stay in the private advisory. A commit that fixes a weakness
|
||
describes the control it adds, not the gap it closes.
|
||
- Every private finding has exactly one public owner, so nothing is tracked only
|
||
in private and nothing is silently dropped.
|
||
- Release notes may describe repaired impact after coordinated remediation,
|
||
without the detail needed to reproduce it.
|
||
|
||
This repository is public. A commit message is a disclosure channel.
|
||
|
||
## Trust model
|
||
|
||
What the server operator can and cannot read, what is end-to-end encrypted,
|
||
how transport and at-rest data are protected, and what beta does not claim are
|
||
stated in one place: [trust-model.md](trust-model.md). Every claim there cites
|
||
the code line or test that makes it true.
|
||
|
||
## Two-Factor Authentication
|
||
|
||
OwnCord supports TOTP-based 2FA:
|
||
|
||
- Users enroll via Settings > Account (QR code + backup codes)
|
||
- Admins can enforce server-wide 2FA via the `require_2fa` setting in the admin panel
|
||
- `require_2fa` requires all users to have 2FA enabled and registration to be closed
|
||
- Login flow returns `requires_2fa: true` with a `partial_token` (10-min TTL, 5-attempt limit)
|
||
- Auth challenges are rate-limited to 10 req/min per IP
|
||
- TOTP code verification uses constant-time comparison (`subtle.ConstantTimeCompare`) to prevent timing side-channel attacks
|
||
|
||
## Account Deletion
|
||
|
||
Users can delete their own account via `DELETE /api/v1/auth/account` with password confirmation. The last admin account cannot be deleted. After 3 failed password attempts, the endpoint locks out for 15 minutes.
|
||
|
||
## Audit Logging
|
||
|
||
Security-relevant actions are recorded in the `audit_log` table with actor, action, target, and detail:
|
||
|
||
- **Auth:** `user_register`, `user_login`, `user_logout`, `login_blocked_banned`, `account_deleted`, `password_change`, `session_revoke`
|
||
- **2FA:** `totp_enabled`, `totp_verified`, `totp_disabled`
|
||
- **Admin:** `role_change`, `role_create`, `role_update`, `role_delete`, `role_reorder`, `user_ban`, `user_unban`, `force_logout`, `setting_change`, `server_setup`, `api_token_create`, `api_token_revoke`, `config_write`, `invite_create`, `invite_revoke`, `plugin_install`, `plugin_uninstall`
|
||
- **Content:** `channel_create`, `channel_update`, `channel_delete`, `channel_perms_update`, `channel_perms_clear`, `channel_user_perms_update`, `channel_user_perms_clear`, `message_delete`, `message_purge`, `emoji_create`, `emoji_delete`
|
||
- **Profile:** `profile_update`, `identity_key_update`
|
||
- **Ops:** `backup_create`, `backup_delete`, `backup_restore`, `ws_connect`
|
||
|
||
Note: `backup_restore` is written synchronously to the live database _before_
|
||
the pre-restore safety copy is taken, so the row survives inside the
|
||
`pre_restore_*.db` backup. The restored database itself will not contain it —
|
||
the restore replaces the database file wholesale.
|
||
|
||
## Client Security Hardening
|
||
|
||
The Tauri desktop client implements the following security measures:
|
||
|
||
### Credential Storage
|
||
|
||
- Credentials are stored in the OS keyring (Windows Credential Manager / macOS Keychain / Secret Service) via the `keyring` crate, with every write read back and verified; if no keyring is available they fall back to an encrypted file (Windows DPAPI with `CRYPTPROTECT_UI_FORBIDDEN`, ChaCha20-Poly1305 elsewhere) — see [credential-storage.md](credential-storage.md)
|
||
- Plaintext passwords are **never** returned to the frontend over IPC — only tokens are accessible from JavaScript
|
||
- Auto-login uses stored tokens for reconnection, not passwords
|
||
|
||
### Tauri Capabilities (Least Privilege)
|
||
|
||
- Filesystem write access is scoped to `$APPDATA/**` and `$APPLOG/**` only
|
||
- DevTools command is gated behind the `devtools` feature flag (excluded from release builds)
|
||
- HTTP fetch is restricted to `https://` origins plus `http://127.0.0.1:*` (the Rust TOFU proxy's loopback tunnel), and **denies** `https://localhost[:*]` and `https://127.0.0.1[:*]` — no legitimate flow reaches loopback over https, so the deny list keeps the renderer from probing other local services
|
||
- `http:allow-fetch` is the **only** URL-scoped HTTP identifier. `tauri-plugin-http` validates the URL exactly once, in the `fetch` command; `fetch_send` and `fetch_read_body` operate on an already-validated `ResourceId` and never consult a scope, so `allow`/`deny` blocks on those identifiers are inert and were removed rather than left in place advertising a control that does not exist
|
||
- The `https://*` wildcard cannot be removed today: link previews (`embeds.ts`) fetch arbitrary user-posted URLs by design, and Tauri scopes per _command_, not per JS caller. Bounded in TypeScript by `isPrivateHost`/`isBlockedForPreview`, a 5 s timeout and a 50 KB body cap; the response is regex-scraped for `og:` tags and never executed
|
||
- Regression-guarded by `tests/unit/capabilities-scope.test.ts`; rationale and the follow-up that would remove the wildcard are in [docs/plans/tauri-capability-narrowing.md](plans/tauri-capability-narrowing.md)
|
||
|
||
### TLS and Certificate Pinning (TOFU)
|
||
|
||
- Self-signed certificates are supported via Trust-On-First-Use (TOFU) pinning
|
||
- The WebSocket proxy (`ws_proxy`) pins the server certificate fingerprint on first connection
|
||
- The LiveKit proxy (`livekit_proxy`) reuses the pinned fingerprint from the WS proxy
|
||
- Certificate mismatch triggers a modal requiring user acknowledgment
|
||
- Update downloads validate `server_url` uses `https://` and rejects URLs with userinfo
|
||
|
||
### Input Validation
|
||
|
||
- IPC commands validate host format, string lengths, and character allowlists
|
||
- PTT virtual key codes are validated to the Win32 range (1–254)
|
||
- LiveKit proxy `remote_host` is validated against CRLF injection
|
||
- API client validates host format before constructing URLs
|
||
- File uploads enforce a MIME type allowlist (images, video, audio, PDF, text)
|
||
- Error messages from server responses are capped at 200 characters
|
||
- Notification titles are sanitized (control chars stripped, length capped)
|
||
|
||
### XSS Prevention
|
||
|
||
- All user-generated content is rendered via `textContent`/`setText` — never `innerHTML`
|
||
- The single `innerHTML` usage (SVG icons) operates on compile-time constants with a runtime guard
|
||
- URLs are validated via `isSafeUrl` (rejects `javascript:`, `data:`, `vbscript:`)
|
||
- YouTube embeds use `sandbox` attribute on iframes
|
||
- `image/svg+xml` is excluded from safe MIME types for data URIs
|
||
- GIF media URLs are validated against the trusted Klipy CDN origins
|
||
- Linkified URLs strip trailing punctuation to prevent misleading destinations
|
||
|
||
### Search and Rate Limiting
|
||
|
||
- Client-side search requests are rate-limited (500ms minimum interval + 300ms debounce)
|
||
|
||
## Known Limitations
|
||
|
||
- Server auto-updates depend on a dedicated pinned minisign/Ed25519 server release key in [Server/updater/server_update_public_key.txt](../Server/updater/server_update_public_key.txt) and a signed release manifest that binds the shipped binary hash to the release version; Windows Authenticode/SmartScreen code signing is still separate work
|
||
- CSP `connect-src` allows `https:` to any host (necessary for self-hosted server URLs not known at build time). Because of this, narrowing the Tauri `http:allow-fetch` scope alone would not bound exfiltration from a compromised renderer — the webview's own `fetch` reaches the same hosts without going through the plugin. Closing that requires narrowing `connect-src` and moving the link-preview fetch into Rust in the same change
|
||
|
||
## Security Hardening Checklist for Operators
|
||
|
||
- [ ] Enable TLS (self-signed is the default; custom certs recommended for production)
|
||
- [ ] Keep invite-only registration enabled (default)
|
||
- [ ] Set a strong admin password
|
||
- [ ] Configure rate limits (defaults are sensible but review for your use case)
|
||
- [ ] Run regular backups via the admin panel
|
||
- [ ] Keep the server updated (admin panel shows available updates)
|
||
- [ ] Firewall: only expose port 8443 (HTTPS); for voice/video also 7880-7881/TCP and 50000-60000/UDP (LiveKit signaling + media — see [deployment.md](deployment.md)); port 80 only when using ACME
|
||
- [ ] Enable server-wide 2FA requirement once all users have enrolled
|
||
- [ ] Set `admin_allowed_cidrs` to restrict admin panel access to trusted networks
|