You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two High + four Medium + one Low from a manual audit of the v0.11
surfaces. No critical bugs found; these are defense-in-depth /
scope-mismatch / footgun fixes. Test suite: 861 / 861 pass.
H-1 (MCP run_sql WITH-CTE write-bypass)
src/mcp/admin-write-tools.ts — the allow_write:false gate previously
only checked the prefix `^\s*(WITH|SELECT|EXPLAIN|PRAGMA)\b`. SQLite
supports data-modifying CTEs (`WITH x AS (SELECT 1) UPDATE foo …`)
which start with WITH and bypass the gate, executing on the writable
raw connection. Mirror sql-runner.ts: strip comments + string
literals, then scan for any of INSERT|UPDATE|DELETE|DROP|ALTER|
CREATE|REPLACE|TRUNCATE|ATTACH|DETACH|REINDEX|VACUUM. Explicit error
names the offending keyword.
H-2 (list_settings / get_setting scope leak)
src/mcp/admin-write-tools.ts — both tools advertised mcp:read scope
but returned decrypted secrets (smtp.password, oauth2.<provider>.
client_secret, FCM service-account JSON, OneSignal app key, …). The
mcp:read scope is the least-privileged read scope used for observer/
auditor tokens — it must not see operator secrets. Raised both to
mcp:admin. Test updated to match the new contract.
M-1 + L-1 (records sort SQL-identifier injection + naive quoteIdent)
src/core/records.ts — user-supplied `?sort=…` flowed unvalidated
through a local quoteIdent that didn't escape embedded `"`. Single-
statement prepare blunted classic injection today, but error-based
oracles + future API changes (multi-stmt exec) would turn it into
RCE. Fix: whitelist sort columns against the actual collection's
field set (id/created_at/updated_at + non-system fields). View
collections fall back to a regex check since their schema is inferred
from the SELECT. Local quoteIdent now matches collections.ts (doubles
embedded `"` per spec).
M-2 (MCP SSE bypasses trusted-proxy)
src/api/mcp.ts — /mcp/events read X-Forwarded-For inline without
consulting VAULTBASE_TRUSTED_PROXIES. Replaced with
trustedClientIp(request, peerIpOf(server, request)). Adapter strips
the placeholder "unknown" trustedClientIp returns when no peer IP is
resolvable (test transports / direct stdio).
M-3 (isOriginAllowed null Origin bypass)
src/server.ts — isOriginAllowed returned true when Origin was null,
on the premise that "same-origin requests omit Origin in some
clients". True for some HTTP fetches, false for browser WebSocket /
EventSource (which always send Origin). Non-browser clients without
Origin were skipping the allowlist entirely. Added a requireOrigin
parameter (default false to preserve existing behaviour); both
/realtime WS and GET /api/v1/realtime SSE call sites now pass true.
M-4 (VAULTBASE_TRUSTED_PROXIES exact-match instead of CIDR)
src/core/sec.ts — docs and SECURITY.md advertised CIDR-equivalent
matching but the impl was Set.has(peerIp) exact-match. Operators
setting VAULTBASE_TRUSTED_PROXIES=10.0.0.0/8 silently got zero
trusted proxies. Re-exported parseCidr/ipInCidr/ParsedCidr from
hook-egress.ts and reused them. List now accepts mixed bare IPs and
CIDR ranges; per-process cache keyed on raw env string.
L-2 (sandbox table-name interpolation)
src/core/sql-sandbox.ts — INSERT INTO main."${obj.name}" SELECT *
FROM _vb_live."${obj.name}" used naive interpolation. Tables created
outside Vaultbase could conceivably have names containing `"`
(assertSqlIdent rejects, but the sandbox copies live state). Wrapped
with a local quoteIdent that doubles embedded quotes.
// FCM service-account JSON / etc. An mcp:read token is the "least-privileged
535
+
// observer" scope and must never see operator secrets.
532
536
reg.register({
533
-
requiredScope: "mcp:read",
537
+
requiredScope: "mcp:admin",
534
538
definition: {
535
539
name: "vaultbase.list_settings",
536
-
description: "List every setting key/value. Encrypted-at-rest keys are decrypted in this response (admin-equivalent visibility) — treat as sensitive.",
540
+
description: "List every setting key/value. Encrypted-at-rest keys are decrypted in this response (admin-equivalent visibility) — requires mcp:admin scope.",
0 commit comments