Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version> (<short-commit>)` into
`dist/sql.html` (graceful `v<version>` 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).
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
105 changes: 105 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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
`<http_handlers>` 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://<host>/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 `<http_handlers>` 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' <issuer-origins>`** — 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 `<token_processor>` 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.