Vaultbase has three principal classes:
- Anonymous / unauthenticated — public records only (
view_rule = null). - User (auth-collection JWT) — gated by per-collection rule expressions.
- Admin (
vaultbase_adminrow) — bypasses all rule checks; can author hooks, custom routes, scheduled jobs, queue workers, view collections. Admins are trusted with code execution on the host process.
Treat the admin role as operator-equivalent. Compromise of any admin account is equivalent to root on the box.
Each of these compiles admin-supplied JS via new AsyncFunction("ctx", code) and runs it in the same process as the API server:
- Hooks —
vaultbase/src/core/hooks.ts— fired on record create/update/delete. - Custom routes —
vaultbase/src/core/routes.ts— mounted under/api/custom/<path>. - Cron jobs —
vaultbase/src/core/jobs.ts— scheduled execution. - Queue workers —
vaultbase/src/core/queues.ts— pulled from thevaultbase_jobs_logqueue. - View collections —
vaultbase/src/core/collections.ts::createUserView— admin-supplied SQL backs aCREATE VIEW. ThevalidateViewQueryguard rejects DDL/DML keywords and obvious abuse, but a determined admin can still read sensitive tables in theirSELECT.
Each compiled function gets a helpers object including helpers.http /
helpers.fetch (network egress, through the guard below) and helpers.query
/ helpers.db (database access with no rule checks).
Code in hooks, custom routes, cron jobs and queue workers is trusted as an admin. It runs in the server process with the server's privileges and is not sandboxed:
helpers.query,helpers.findandhelpers.db.*read and write every collection and system table directly; collection rules do not apply.helpers.fs.*andhelpers.os.envreach the server's files (database,.secret, uploads) and environment (VAULTBASE_JWT_SECRET,VAULTBASE_ENCRYPTION_KEY).- Plain JavaScript in the function body can do anything the runtime allows,
including
import()of Bun modules.
So creating or editing such code is equivalent to shell access on the host,
and the admin accounts and mcp:admin / admin API tokens that can do it must
be protected accordingly. The egress guard below limits accidental SSRF
from hook code that fetches user-supplied URLs; it is not a sandbox for
hostile code. Data a hook reads from a request (ctx.record, ctx.request)
is untrusted: validate it before using it in helpers.db.exec SQL, file
paths or outbound URLs.
helpers.http SSRF guard (since v0.1.4): every request runs through
core/hook-egress.ts::assertEgressAllowed before fetch(). The default deny
list blocks RFC1918 private space (10/8, 172.16/12, 192.168/16), CGNAT
(100.64/10), loopback (127/8 + ::1), link-local (169.254/16, fe80::/10 —
covers AWS / GCP / Azure metadata), and IPv6 unique-local (fc00::/7).
Operator overrides via Settings:
hooks.http.deny— comma-separated CIDR list replacing the default (opt-in to a thinner list when the default blocks legitimate internal services). Set to the literal"off"to disable filtering entirely.hooks.http.allow— comma-separated CIDR list evaluated after deny. Punches a hole through (e.g.)127.0.0.0/8for a specific sidecar without disabling the rest of the loopback block.
The guard is defense-in-depth — it does not replace kernel-level egress
filtering. For hardened deployments still run vaultbase under a network
namespace / nftables egress filter as belt-and-braces. Known limitation:
DNS rebinding (attacker controls the hostname's DNS, flips the answer
between our resolve check and fetch()'s internal resolve). Use
kernel-level filtering when you need bulletproof guarantees.
Admin-configured integration URLs are trusted. The SMTP host, S3 /
storage endpoint, OAuth/OIDC endpoints (oauth2.* URLs, including a custom
OIDC token and userinfo URL) and push-notification providers are set by
admins and are not passed through the egress guard: they often point at
internal services (Mailpit, MinIO, a corporate IdP) on purpose. Anyone who
can change these settings can make the server connect anywhere and can
redirect a stored secret (SMTP password, client secret) to a host they
control, so the settings API is admin-only for that reason too. Webhooks
(whose deliveries carry record data) and hook/route/job code do use the
guard.
helpers.fetch goes through the same guard as helpers.http (it used to be
raw fetch): every redirect hop is re-checked, only http:/https: are
allowed, and credentials are dropped on a cross-origin redirect.
helpers.query reads through the records layer without collection rules
(see the trust boundary above); it makes no network requests.
JWTs are HS256 signed with VAULTBASE_JWT_SECRET (or a generated <dataDir>/.secret fallback at mode 0600). Every token now carries:
iss = "vaultbase"(verified)aud∈ {admin,user,file}jti(UUID) — checked againstvaultbase_token_revocationson every verifyiat,exp
Verification path is centralized in core/sec.ts::verifyAuthToken and rechecks:
jtinot in revocation list- principal row still exists (admin/user)
password_reset_at <= iat(admin)
POST /api/auth/logout revokes the bearer token's jti. Token rotation uses a sliding-refresh window via POST /api/auth/refresh.
To force-logout every admin (after suspected compromise), update password_reset_at to unixepoch() for that admin row; tokens issued before that timestamp will fail verification on next request.
Local filesystem mode validates uploaded filenames as ^[uuid]\.[ext]{1,12}$ and resolves every path inside vaultbase_data/uploads. Path-traversal attempts are rejected at the storage layer (core/storage.ts::safeLocalPath).
Files served via GET /api/files/:filename always evaluate the parent record's view_rule regardless of whether the field is protected. Image MIME types render inline; everything else gets Content-Disposition: attachment plus X-Content-Type-Options: nosniff to neutralize stored-XSS via uploaded HTML/SVG.
/realtime (WebSocket) and GET /api/realtime (SSE) reject upgrades whose Origin is not in the security.allowed_origins setting (comma-separated). Empty setting = same-origin only.
Cross-origin API consumers should set security.allowed_origins and supply a CORS plugin in front of Elysia (out of scope here).
Per-IP token-bucket rate limiting honors X-Forwarded-For only when the immediate peer is in the VAULTBASE_TRUSTED_PROXIES env (CIDR-equivalent, comma-separated). Otherwise the socket peer IP is used. Set this env when running behind Cloudflare, AWS ALB, nginx, etc. — leaving it unset means a hostile client cannot spoof XFF, but any proxy-derived IP also won't be honored.
While no admin exists, POST /api/v1/admin/setup requires a setup key (X-Setup-Key header or setup_key in the body), so nobody who reaches a fresh install before the operator can claim it. The key is VAULTBASE_SETUP_KEY when set; otherwise the server generates one, stores it in <dataDir>/.setup-key (mode 0600) and prints it at startup. The file is deleted once the first admin exists. vaultbase setup-admin creates an admin locally without the key.
The setup endpoint also enforces a 12-character minimum password and atomically refuses if a concurrent setup landed first.
imagescriptand@jsquash/{webp,avif}decode untrusted user image input. Upstream advisories should be monitored. Consider running thumbnail generation in a child process so a decoder OOM does not kill the API.quill2.0.x andmonaco-editor0.55.x ship custom decoration / hover APIs that have historical XSS risk; audit any custom integrations before exposing rich-text/code editing to non-admin users.@types/bunis pinned to a SemVer range, notlatest.
- Set
VAULTBASE_JWT_SECRET(don't rely on.secretfallback). - Create the first admin with
vaultbase setup-admin, or with the setup key printed at first boot (orVAULTBASE_SETUP_KEY). - Set
VAULTBASE_HOST=127.0.0.1when a reverse proxy on the same host is the only way in. - Set
VAULTBASE_TRUSTED_PROXIESto your front-door peer IPs. - Set
security.allowed_originsfor the admin SPA / user app origins. - Set
VAULTBASE_ENCRYPTION_KEY(AES-GCM, 32 bytes base64) if any field type isencrypted. - Configure
oauth2.<provider>.allowed_redirect_urisfor every enabled IdP. - Front the binary with TLS termination that sets HSTS; the static landing/docs CF Pages projects already set
Strict-Transport-Security. - Restrict
vaultbase_data/filesystem permissions to the running user (chmod 700). - Rotate the JWT secret after any suspected compromise: stop the server, delete
.secret(or change the env), restart. All sessions end.