Skip to content

0.10.0

Choose a tag to compare

@cevheri cevheri released this 10 Aug 09:11
· 338 commits to main since this release
Immutable release. Only release title and notes can be modified.

The first release to carry LibreDB Studio's security programme. Four phases of work landed on main without a version of their own; this is the release that ships all of them, and the reason it is a minor rather than a patch after 67 patches on 0.9.

Read this before upgrading

Three controls are enforced from this version, and each can change the behaviour of a working deployment. None of them requires new configuration in a default setup.

The Content-Security-Policy is enforced, not merely reported. A deployment that serves Monaco or static assets from a non-default origin will see them blocked. CSP_REPORT_ONLY=true is a runtime environment variable and needs no rebuild. A violation appears only in the user's browser console — the policy carries no report-uri, so nothing reaches a server log.

Every state-changing request now needs a matching Origin (or Referer) host, or a JSON content type. An ingress that rewrites Host without setting x-forwarded-host will refuse every action, including login, with a 403 until ALLOWED_ORIGINS names the public origin. Non-browser API clients are unaffected when they send Content-Type: application/json — every published example in docs/API_DOCS.md and scripts/engine-smoke.sh already does, which is how the rule was chosen.

Login is rate limited: five failures per client address per five minutes, and twenty per submitted account per five minutes. A per-account cap is inherently a denial-of-login handle: someone who knows an account address can keep that account locked for the window. RATE_LIMIT_LOGIN_ACCOUNT_MAX=0 disables that bucket entirely.

HSTS is sent unconditionally with a 180-day max-age, no includeSubDomains unless HSTS_INCLUDE_SUBDOMAINS=true, and there is no way to stop sending it. That is deliberate: RFC 6797 makes browsers ignore the header over plain HTTP, and an escape hatch that merely omits it cannot help a deployment that has already been pinned.

What this release closes

Two cross-site-scripting sinks are gone. LLM-generated markdown was being passed to dangerouslySetInnerHTML in the schema-documentation and AI-autopilot panels. Both now render through a React-node renderer that cannot produce HTML. A lint rule keeps a third from being added silently.

An unauthenticated open image proxy is gone. next/image was configured with a wildcard remote host over both HTTP and HTTPS, on a path the middleware left public. Anyone on the internet could use the deployment to fetch arbitrary URLs.

Handler-level authentication no longer depends on the middleware alone. Routes that reach a database or an LLM provider now pass through a single guard that checks the session, applies a rate limit and audits the denial. The inventory is derived by a filesystem walk that demands a written reason for every exemption, so a route added later is refused by default rather than silently unguarded.

Next.js moves to 16.3.0, past five high-severity Middleware/Proxy bypass advisories. src/proxy.ts is Next's middleware and is exactly where this programme put authorization, so that bump is not routine dependency hygiene.

Credentials are encrypted at rest

When STORAGE_PROVIDER is sqlite or postgres, six credential-bearing fields are sealed with AES-256-GCM before they reach the store: password, connectionString, ssl.clientKey, and the SSH tunnel's password, privateKey and passphrase. host, port, user and database stay readable on purpose, so an operator can still answer which database a dump belongs to.

The key comes from STORAGE_ENCRYPTION_KEY when set, and otherwise from JWT_SECRET through HKDF-SHA256. Nothing is required of you — that fallback is what keeps the zero-config promise every distribution channel depends on.

Two consequences worth knowing before you meet them:

  • Rotating JWT_SECRET with no explicit STORAGE_ENCRYPTION_KEY set makes every stored credential unreadable. Set STORAGE_ENCRYPTION_KEY first if you expect to rotate.
  • A credential that cannot be decrypted is omitted, not fatal. The record survives, one warning is logged per read, and the user retypes one password. Throwing instead would take query history, saved queries and charts down with it.

Migration is lazy: reads accept plaintext and enveloped values, writes always produce enveloped values. There is no migration command, no backfill, and no version column.

What this does not protect

Encryption at rest is not a vault. Anyone who can read the process environment can read the credentials.

It protects a stolen database file or dump. It protects a backup or volume snapshot only when STORAGE_ENCRYPTION_KEY comes from outside the mounted volume — because with the fallback, the key derives from a JWT_SECRET that the auth bootstrap persists next to the store, so a snapshot of the data directory would contain both.

The browser's localStorage copy of credentials stays plaintext, by deliberate product decision. That is what lets Studio work without a master password, and it is why the cross-site-scripting work above carries the weight it does.

The rate limiter counts per process: N replicas allow N times the budget. Multi-replica deployments should enforce at the ingress.

Audit trail

Every authentication transition — login success, login failure, logout, the OIDC callback's success and each of its five distinct failure reasons, a rate-limit trip, an authorization denial — emits one structured JSON line on stdout under the libredb.audit.v1 schema, retained by whatever collects container logs. The emitted field set is an explicit allowlist, so a field added to an event cannot start being logged by accident, and URI credentials are collapsed to scheme and host before anything is written.

A failed login records the submitted email verbatim. Volume from unauthenticated probes is metered, but the meter is keyed on the client address; operators exposed directly to the internet should cap their log driver size.

Supply chain

  • Secret scanning on every pull request and a full-history sweep on a schedule. The history was swept before any configuration was written: 24 matches across 753 commits, every one read and classified as fabricated — jwt.io's published sample token, hex filler, documentation examples, and PEM bodies whose content literally reads fake. No credential has ever been committed to this repository, and each of those 24 is suppressed by its exact fingerprint rather than by a path, so a real secret added later in the same file still produces a new finding. The sweep still scans clean across the full history, now 774 commits.
  • Dependency and container scanning, reporting on pull requests and gating outside them.
  • A CycloneDX 1.7 software bill of materials is attached to every release from this one onward, attested, and required by name before a release can publish.
  • The production build type-checks. typescript.ignoreBuildErrors is gone.

A published security posture you can check

docs/SECURITY.md lists sixteen controls, each linked both to the code that enforces it and to the test that verifies it. bun run security:check runs in CI and fails when that page and the repository disagree in either direction — a control naming a test that does not exist, or a security test no control accounts for. The page also states its own limits, including the ones above.

Helm chart: 0.1.31

appVersion moves to 0.10.0 and the chart is marked as carrying security updates. No template or values change, so an existing values.yaml keeps working. If your ingress rewrites Host, read the ALLOWED_ORIGINS note above before upgrading; helm upgrade prints the same guidance.

helm repo add libredb https://libredb.org/libredb-studio/
helm upgrade --install libredb-studio libredb/libredb-studio --version 0.1.31

Full changelog: 0.9.67...0.10.0