Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 21:49
· 16 commits to main since this release

Binaries for Linux, macOS, Windows and FreeBSD are attached below. The Docker image is
ghcr.io/mariuz/firebirdrest:0.3.0. See CHANGELOG.md for details.

Production readiness: Prometheus metrics, Server-Timing, JWKS keys from identity providers
with a token cache, single values and files in any media type, raw bodies for RPC, and the fixes
of a security review. A JavaScript client with the supabase-js API,
firebirdrest-js, checks compatibility in CI.

Behaviour changes to check before upgrading:

  • server-cors-allowed-origins = "*" no longer sends Access-Control-Allow-Credentials; list
    the origins explicitly if browsers must send credentials.
  • GET / describes only what the caller's role is granted; set
    openapi-mode = "ignore-privileges" for the old document.
  • Requests are limited to 60 seconds and 16 MB bodies (server-request-timeout,
    server-max-body-size), and and=/or= nesting to 16 levels, in.() to 1000 values.
  • Permission errors no longer include Firebird's "Effective user is ..." line, and unexpected
    errors return a generic message (the cause is logged).
  • Pool and statement cache metrics label roles in upper case.
  • Responses carry X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy.

Security

A review of the SQL generation, authentication and HTTP surface found no injection or
authentication bypass, and these weaknesses, all fixed here:

  • A deeply nested and=/or= filter or a long in.() list could take the Firebird connection
    down or burn CPU for tens of seconds: nesting is limited to 16 levels and in.() to 1000
    values (400 beyond).
  • A Range header with a huge end overflowed the limit arithmetic and removed db-max-rows;
    such ranges are refused with 416.
  • A single value requested as text/html (or SVG, XML, script) was served as a page of the
    API's origin: stored script in a column could run there. Every response now carries
    X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy, and those types
    are sent as downloads.
  • With the default server-cors-allowed-origins = "*", any origin was reflected together with
    Access-Control-Allow-Credentials: true; the wildcard now answers * without credentials, and
    only listed origins get credentials (with Vary: Origin).
  • GET / described every table and routine to anonymous callers. The document now follows the
    caller's privileges (openapi-mode, default follow-privileges).
  • Permission errors named the database login ("Effective user is ..."), and unexpected errors
    returned their internal text; both are now generic for the client and logged on the server.
  • Each case spelling of a role name (web_user, WEB_USER...) opened its own connection pool;
    names are folded to upper case, as Firebird does.
  • The HTTP server had no request timeout and read request bodies inside the transaction:
    server-request-timeout (60 s), server-max-body-size (16 MB) and header limits are
    enforced, and bodies are read before the transaction starts.
  • An empty boolean in the config file (admin-pprof =) counted as true; it now keeps the
    default.
  • admin-server-host binds the admin server to its own address, e.g. 127.0.0.1.

Added

  • Metrics in the Prometheus format at GET /metrics on the admin port: requests by method and
    status, a request duration histogram, error codes, connection pool and statement cache usage
    per role, schema cache loads, JWT cache and JWKS fetches, and Go runtime figures. See
    Monitoring.

  • server-timing-enabled adds a Server-Timing header (jwt, parse, transaction,
    response), as in PostgREST.

  • jwt-secret accepts a JWK or a JWKS as JSON, PEM certificates and PKCS#1 RSA keys, several PEM
    keys at once, Ed25519 keys (EdDSA) and @/path/to/file; PS256/384/512 tokens are accepted
    with RSA keys. Tokens with a kid are checked against the matching key.

  • jwt-secret-is-base64 for base64 encoded HMAC secrets.

  • jwt-jwks-url fetches the signing keys of an identity provider (Auth0, Keycloak, Okta, Azure
    AD, ...) at startup and every jwt-jwks-refresh seconds, and again when a token names an
    unknown kid, so rotated keys work right away.

  • A JWT validation cache (jwt-cache-max-entries, default 1000) skips the signature check for
    tokens already accepted, until they expire or for 5 minutes.

  • Single values as is: any concrete Accept type that isn't a built-in format (text/plain,
    application/octet-stream, image/png, application/geo+json...) returns the one selected
    value of the one matching row as the body, with that Content-Type. Binary columns are sent
    as their bytes, so files can be served straight from the database. Works for reads, writes
    with return=representation and RPC.

  • Raw request bodies for RPC: a POST /rpc/<name> whose Content-Type isn't JSON or a form puts
    the body into the routine's only parameter, as text or bytes (FBRST133 otherwise).

  • The example database has a covers table with a binary BLOB and a blob_size function.

  • A raw single-value response sends a binary value as the driver returns it, without a base64
    encode/decode round trip, so serving a file holds one copy of it in memory.

Fixed

  • Accept entries with a parameter that has no value, such as the
    application/vnd.pgrst.plan+text; for="application/json"; options=; that postgrest-js sends
    for explain(), were rejected with 406. The media type is now read even when a parameter
    is malformed.

  • Responses are sent only after the transaction commits. A failed COMMIT now returns an error
    instead of a success response, and headers set before an error (such as Content-Range on a
    416) no longer appear on the error response.

  • Graceful shutdown waits for requests in flight (up to 10 seconds) before closing the database
    connections; before, requests running at SIGTERM could fail with 500.