From c2e1e17c9cd71fe04cf278d953af8bc23e23b709 Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Mon, 29 Jun 2026 13:36:06 +0200 Subject: [PATCH] docs: add SECURITY.md (disclosure policy + config.json threat model) (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release-blocker for 1.0 (#68). Adds a vulnerability-disclosure policy (GitHub private advisory / security@altinity.com, latest-release support window) and formalizes the threat model the README only described prose-style: - config.json is served to browsers → treat as public; prefer a PKCE public client (the supported install.sh renders a secret-free config by construction); if a client_secret is unavoidable, lock the redirect URI to exactly https:///sql (mirrors CLAUDE.md hard rule 3 + README). - Token handling: id/access/refresh + PKCE state/verifier live in sessionStorage (tab-lifetime), never localStorage/cookies. - CSP baseline: default-src 'none' with connect-src bounding exfiltration; ship deploy/http_handlers.xml's headers. - Operator responsibilities (ClickHouse RBAC, IdP config, TLS) called out as out of scope for the client. Also links SECURITY.md from the README "Security headers" section and notes it in the CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PgV4UResR7braUkAq7VaCr --- CHANGELOG.md | 3 ++ README.md | 4 ++ SECURITY.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 SECURITY.md diff --git a/CHANGELOG.md b/CHANGELOG.md index cbecf14..8140455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ auto-generated per-PR notes; this file is the curated, human-readable history. ## [Unreleased] ### Added +- `SECURITY.md`: private vulnerability-disclosure policy + the `config.json` + threat model (it's served to browsers — prefer a PKCE public client; lock the + redirect URI if a `client_secret` is unavoidable) and the CSP/token baseline (#72). - In-app build stamp: the build bakes `v ()` into `dist/sql.html` (graceful `v` fallback when not a git checkout) and shows it in the user menu, so a bug report can be tied to an exact build (#74). diff --git a/README.md b/README.md index 3f39d13..1714654 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,10 @@ wrong password is surfaced on the login screen — the connect probe runs a ### Security headers +> For the vulnerability-disclosure policy and the full threat model (why +> `config.json` is public, the redirect-lock requirement, token storage), see +> [`SECURITY.md`](SECURITY.md). + `deploy/http_handlers.xml` sends a strict **Content-Security-Policy** plus `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer` on the SPA response. The CSP is `default-src 'none'` with everything re-allowed explicitly: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..29d8541 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,105 @@ +# Security Policy + +The Altinity SQL Browser is a single self-contained HTML file (no application +backend) served from a ClickHouse cluster's `user_files/` by an +`` static rule. It talks only to that ClickHouse server and your +OAuth IdP, and makes zero third-party requests. The notes below describe how to +report a vulnerability and the threat model you should deploy against. + +## Supported versions + +This project is pre-1.0 and ships from a single `main`. Security fixes land on +`main` and in the next tagged release; only the **latest release** is supported. +There are no long-term-support branches before 1.0. + +| Version | Supported | +|--------------------|-----------| +| Latest release | ✅ | +| Older releases | ❌ | + +## Reporting a vulnerability + +**Please do not open a public GitHub issue for a security vulnerability.** + +Report privately, either way: + +- **GitHub private advisory** (preferred): on this repository, go to + **Security → Advisories → Report a vulnerability**. This opens a private + thread with the maintainers. +- **Email**: `security@altinity.com`. Please include "altinity-sql-browser" in + the subject and enough detail to reproduce (affected version/commit — see the + build stamp in the user menu, deploy shape, and steps). + +We aim to acknowledge a report within a few business days and to keep you +updated as we triage and fix. Please give us a reasonable window to ship a fix +before any public disclosure; we're happy to credit reporters who want it. + +## Threat model + +### `config.json` is public — treat it that way + +The app loads its OAuth configuration from `config.json` (served as +`/sql/config.json`), which is **delivered to the browser**. Anything in it is +readable by any user who can reach the page. Never put a value in `config.json` +that you would not publish. + +- **Prefer a PKCE public client (no secret).** Register a "SPA / public / + native" client; the PKCE `code_verifier` authenticates the token exchange, so + no `client_secret` is needed and `config.json` stays secret-free. This is the + recommended shape and what the supported `deploy/install.sh` renders — it + **never writes a `client_secret`**, so a standard install is secret-free by + construction. +- **If your IdP requires a `client_secret`** on the in-browser token exchange + (e.g. a Google "Web application" client), the code accepts it in + `config.json`, but because the file ships to browsers you **must** treat it as + public: **lock the redirect URI to exactly `https:///sql`** with the IdP + and use a suitably scoped consent screen, so a leaked secret can't be replayed + to a different redirect. A secret only enters `config.json` through a + hand-authored config (e.g. an inline `` rule) — apply this rule + wherever you do that. +- **Or front the app with a broker.** An OIDC broker / auth proxy holds the + provider secret and exposes a public PKCE client; the browser talks only to + the broker and `config.json` carries no secret. + +This mirrors the project's contributor rule (`CLAUDE.md` hard rule 3) and the +README's "Configuring OAuth" section. + +### Token handling + +OAuth tokens (id / access / refresh) and the PKCE `state`/`verifier` used during +the redirect round-trip are kept in **`sessionStorage`**: scoped to the browser +tab and cleared when the tab closes. Tokens are **never** written to +`localStorage` or cookies. There is no server-side session. + +### Browser-hardening baseline (CSP and headers) + +`deploy/http_handlers.xml` serves the SPA with a strict +**Content-Security-Policy** plus `X-Content-Type-Options: nosniff` and +`Referrer-Policy: no-referrer`. The CSP is `default-src 'none'` with everything +re-allowed explicitly; the load-bearing directive is: + +- **`connect-src 'self' `** — bounds where the page may send + data, so an injected script cannot exfiltrate the `sessionStorage` tokens to + an attacker-controlled host. `'self'` covers ClickHouse queries + + `config.json`; the issuer origins cover OIDC discovery and the token endpoint. + `deploy/install.sh` fills this list automatically from your issuer's OIDC + discovery document (for a manual non-Google install, edit the `connect-src` + line in `deploy/http_handlers.xml`). +- `frame-ancestors 'none'` (anti-clickjacking), `base-uri 'none'`, `img-src + data:`, and a `sandbox=""` (script-less, inert) `srcdoc` iframe for the + result cell-detail HTML preview. + +If you deploy the SPA without this handler, you lose these protections — +**ship the provided CSP/headers** (or equivalents). + +## Operator responsibilities (out of scope here) + +The SPA is a client. The following are configured and secured by whoever +deploys it, not by this project: + +- **ClickHouse access control** — what a signed-in user can read/run is governed + by ClickHouse RBAC / grants and the `` JWT validation, not by + the browser. The UI cannot grant access the server doesn't already allow. +- **IdP configuration** — client type, redirect-URI allowlist, consent scopes. +- **TLS termination** — always serve the page and the ClickHouse endpoint over + HTTPS.