Release Notes - v0.11.0
SnackBase v0.11.0 is a security and platform release. It remediates every
Critical, High and Medium finding from the 2026-08-09 VAPT, adds optional
trusted-issuer platform authentication with an integrated Cloud Studio, moves
the runtime to Python 3.14, and ships a substantially smaller Docker image.
🛡️ Security remediation (VAPT 2026-08-09)
🔴 Critical
- C-01 — File path traversal.
FileStorageServiceaccepted
"<B>/../<A>/f.txt", which satisfied the account-prefix check and the
storage-root check while landing in a sibling tenant's directory. Resolved
paths are now confined to the caller's own account directory with
is_relative_to, and the S3 provider rejects dot segments in object keys. - C-02 — Custom-endpoint aggregate injection. The aggregate action
interpolatedcollection,group_byandfielddirectly intotext()SQL,
so a template-expanded request value could read another tenant's rows.
Aggregates now resolve the collection throughCollectionRepository, validate
against the collection schema, and execute through
RecordRepository.aggregate_records, which quotes identifiers and ANDs the
account clause.collectionnow means a collection name, as documented, and
the previously ignoredfilteris compiled and applied. - C-03 — Default signing secrets accepted in production.
secret_keyand
token_secretcould stay on the published placeholder values, so a
production instance could sign every JWT and API key with a secret in the
repository. A productionmodel_validatornow fails closed and names each
variable still on its default.SNACKBASE_TOKEN_SECRETwas previously
undocumented and is now in.env.exampleanddocker-compose.yml. - C-04a — Function deploy-time build isolation.
uv pip installinherited
the full environment, exposingSNACKBASE_SECRET_KEY,
SNACKBASE_ENCRYPTION_KEYandSNACKBASE_DATABASE_URLto any source
distribution's build backend. Builds now receive only whatuvneeds, tenant
pins install with--only-binary :all:so no attacker-authoredsetup.py
runs, andfunction_dependency_modedefaults toallowlist. - C-04b — Function invoke sandbox. Invoked handlers ran as ordinary child
processes with full filesystem and network reach. Three kernel-enforced
bounds replace that: Landlock confines the child to the interpreter, its own
read-only function env and its per-invoke work directory;setrlimitbounds
address space, CPU and open files; and the egress policy moved down to
socket.connectso every Python networking path shares it. Governed by
SNACKBASE_FUNCTION_SANDBOX_MODE(auto/required/disabled), where
autoresolves torequiredin production.
🟠 High
- H-01 — Webhook SSRF. The hand-written RFC1918 deny list left cloud
instance metadata (169.254.169.254),0.0.0.0/8, CGNAT and IPv4-mapped
IPv6 literals reachable, checked only literal IPs, and reflected 5000
characters of the destination's response body. Classification now uses
ipaddressspecial-use properties, hostnames are resolved with every A/AAAA
answer checked, and sends go through a transport pinned to the approved
address (preserving Host and SNI) so DNS cannot be re-answered between
validation and connect. Redirects are refused andtest_webhookreturns only
a status code. - H-02 — Unbounded online password guessing. Login had no effective
throttle. Two layers now apply: a per-client-IP failure budget charged only by
failed attempts, and a per-account lockout persisted on the user row with
doubling backoff. Both live in the login handler so no authenticated-caller
bypass reaches around them, client-IP derivation is trusted-proxy aware, and
rate limiting now defaults to on. - H-03 — SAML assertion binding. All three providers verified the XML
signature and then read the assertion, accepting captured assertions
indefinitely and assertions minted for any other SP on the same IdP.
validate_assertionnow enforcesNotBefore/NotOnOrAfterwith configurable
clock skew, a requiredAudienceRestrictionnaming this SP,
SubjectConfirmationDatarecipient and expiry, and an assertion-ID replay
cache claimed last.
🟡 Medium
- M-01 — Realtime bypassed read rules. Broadcasts filtered only on account
and subscription, so subscribers received rows their view rule excluded,
fields outside their projection, and unmasked PII.RealtimeReadPolicynow
reuses the REST read path's controls — view rule, row re-read, field
projection and PII masking — and reads role and groups fresh per event.
Deletes deliver only the record id. - M-02 — Upload MIME validated from the client header. Types are now
detected from file content withpuremagic; only signature-less types
(text/plain,text/csv,application/json) are taken on the caller's word
and must decode as UTF-8. Stored filenames use a UUID plus the detected
type's extension in both the local and S3 providers. - M-03 — Uploads buffered whole before the size check. Uploads are read
throughbuffer_upload_within_limit: an over-limitContent-Lengthis
refused before a byte is read, the body is consumed in 1 MiB slices, and
accepted uploads spool past the first chunk. - M-04 — File downloads authorized by path prefix. Uploads are now
registered in afilestable and a download requires a claim: the uploader,
or a record referencing the path that passes the caller'sviewrule
(evaluated by the existingcheck_collection_permission). Superadmin keeps
its bypass. - M-05 — Refresh-token replay. A replayed refresh token now revokes every
refresh token for that user, forcing both parties to re-authenticate. - M-06 — Invitation tokens stored in plaintext. The column holds a SHA-256
hash and lookups hash the presented value. Resending rotates the token,
invalidating any link in flight; listing no longer returns a token, and the
admin UI's copy-link action goes through resend. - M-07 — Login as an account/IdP oracle. A password attempt against an
SSO-only user returned the provider name and redirect URL. Both that case and
an unknown address now return an identical generic 401 and charge the failed
attempt budget. - M-08 — Anonymous reachability. Accepted, not fixed. An
allow_anonymous
opt-in was implemented and then reverted: setting an empty rule is already the
opt-in, and both the rules and the flag were superadmin-owned, so the flag
moved no decision to a different principal. The residual risk — collections
and rules are global, so opening one exposes every account with rows in it —
is recorded in the VAPT roadmap and under "Accepted findings" in the security
README. - M-09 — Image built on an interpreter CI never validated. Both Python
stages now build on3.14-slim, matching.python-version, and every base
image (including theuvimage used byCOPY --from) is digest-pinned. - M-10 — Production JSON logging crashed at boot.
basicConfigrejects
streamandhandlerstogether; the redundantstreamargument is removed,
so the shipping default (production + JSON) boots.
🧪 Security test suite
- Added a two-tenant security harness with per-surface fixtures (files,
collections, endpoints, functions),assert_denied/assert_no_leak/
assert_allowedhelpers, across_tenant_matrixattack helper, and a
securitypytest marker applied automatically totests/security/. - Every Critical, High and Medium finding traces to a named guard, documented in
tests/security/README.md. The suite now carries noxfailmarkers, so a
VULNERABLE row means a regression rather than a known gap. - Fixed the security HTML report, which previously hardcoded PASSED for every
test including the 51 guarding confirmed vulnerabilities. Outcomes now come
from apytest_runtest_makereporthook and map to an audit vocabulary
(xfail→ VULNERABLE, strictxpass→ FIXED), with findings grouped by VAPT
ID and requests labelled ALLOWED/DENIED by what the server did. - Added a Tests workflow running the security suite as its own job with the
consolidated HTML report uploaded as an artifact.
☁️ Platform authentication and Cloud Studio
- Added optional trusted-issuer authentication: instances can accept RS256/ES256
access tokens from a configured external issuer (SnackBase Cloud or a
self-hosted OIDC provider) viaSNACKBASE_PLATFORM_ISSUER,
SNACKBASE_PLATFORM_JWKS_URLandSNACKBASE_PLATFORM_AUDIENCE. Every
SNACKBASE_PLATFORM_*setting defaults to unset, so an unconfigured instance
behaves exactly as before — no JWKS fetch, no alternate auth path. - Platform principals are supported on both single- and multi-tenant instances.
A platform principal is the operator of the instance and resolves into the
system account (SY0000); onlysnackbase_role: adminis accepted. - An existing bootstrap superadmin with the same email is adopted unmodified
rather than duplicated, so break-glass password login keeps working. - Merged the Cloud Console into
ui/as a dual-mode build. Studio migrated to
@snackbase/sdkwith runtime platform configuration, behind a single lazy
route import gated byIS_PLATFORM, with an integration test asserting that
platform-only code is eliminated from self-host production bundles. - Added a stable environment reference for platform routing and authentication,
with tests for reference generation and validation.
🐍 Runtime and packaging
- Upgraded the runtime to Python 3.14 (
.python-version, Ruff
target-version, mypy), switchinghook_registryto
inspect.iscoroutinefunction. - Function deploy-time syntax validation now parses with the running
interpreter's version, so Python 3.14 syntax (t-strings) is accepted. - Slimmed the Docker image with a three-stage build — frontend builder, Python
dependency builder, and a runtime stage that no longer ships
build-essential/gcc (saves ~300 MB). The runtime keeps the pinneduv
binary,rufffor dynamic collection migrations, andpackages/snackbase_fn
for Functions env installs; the app starts via.venv/bin/uvicorn. - The frontend image stage now pins the published
@snackbase/sdk0.9.0 and
@snackbase/react0.6.0 rather than resolvingfile:workspace deps, fixing
collections.listPaginated is not a functionin containers. - Moved
types-aiobotocore-sesto the dev dependency group and tightened
.dockerignoreto shrink the build context.
🔧 Fixes and housekeeping
- The SPA no longer answers API paths, so unmatched API requests return an API
response instead of HTML. - Audit log endpoints accept requests without a trailing slash; collection rules
are registered under the collections router. - Collapsed the byte-for-byte duplicate at
packages/snackbase_fn/snackbase_fninto a symlink ofsrc/snackbase_fn, so
runtime fixes land in the copy function envs actually install. - Removed the stray empty
src/__init__.pythat made mypy resolve every module
under two names and abort before checking anything. - Refreshed E2E tests with a reworked auth flow, a reusable
delete-collection-by-name helper, anddata-testidhooks on the collections
page. - Updated API documentation for the
sb_ak.<payload>.<signature>API key format
and the/api/v1/admin/api-keysand/api/v1/records/{collection}paths. - Updated application version reporting and documentation to
0.11.0.
⚠️ Upgrade notes
- Rate limiting now defaults to on. Review
SNACKBASE_RATE_LIMIT_PER_MINUTEand, if you run behind a proxy, configure
the trusted-proxy setting soX-Forwarded-Foris honoured — it is believed
only from a configured peer and defaults to loopback. SNACKBASE_TOKEN_SECRETandSNACKBASE_SECRET_KEYmust be set to
non-default values in production, or the instance refuses to boot. Generate
withopenssl rand -hex 32.- Function dependency installs now default to
allowlist. Name the packages
your tenants may install, or setfunction_dependency_mode=open_pinnedto
restore the previous behaviour. - Function invocation requires a Landlock-capable kernel in production.
SNACKBASE_FUNCTION_SANDBOX_MODE=autoresolves torequiredthere and
refuses invokes rather than running handlers on the bare host. Setdisabled
only where you accept that. - Invitation tokens are one-way hashed. Resending an invitation rotates the
token and invalidates any link already in flight; listing invitations no
longer returns a token. - File downloads now require a claim. Attachments are reachable by their
uploader or through a record the caller may view. Files uploaded before this
release are not in thefilestable, so only the record-reference path
authorizes them. - Custom-endpoint aggregate actions take a collection name, not a physical
table name, and theirfilteris now applied instead of ignored. Review any
aggregate endpoint configuration. - Run the normal migration upgrade before starting:
uv run python -m snackbase migrate upgrade. - The runtime is now Python 3.14. Self-hosted installs that pin an interpreter
need to update it.