fix(settings): verified authz on settings routes — close unauthenticated write (Critical) - #2848
Merged
Merged
Conversation
… (Critical) The settings HTTP routes trusted spoofable identity headers (x-user-id / x-tenant-id / x-permissions) and the write path (setMany) had NO permission gate. On a standard `os serve --server` (settings + HTTP server composed by default, routes on the raw app with no auth middleware) an UNAUTHENTICATED remote client could write tenant- or platform-scoped settings — including the auth security-policy manifest — and enumerate every namespace. - Verified identity: SettingsServicePlugin derives the caller's identity and capabilities from the platform's verified resolution (resolveAuthzContext: session cookie / API key / OAuth), never from request headers. The route default is now SECURE (anonymous, denied). - Capability gates: manifest readPermission/writePermission enforced for HTTP callers (reads of a protected namespace, writes, and actions require the declared capability; writes default to at least the read capability, never ungated). Gated behind an `enforced` flag set only at the HTTP boundary — in-process/boot callers (kernel.getService, seed) are unchanged and keep trusted access. - Unauthenticated HTTP → 403 SETTINGS_FORBIDDEN. Found by an adversarial review of the request→ExecutionContext trust boundary. Tests: service-level enforced-mode gates + route-level deny (anonymous read hidden, write 403, spoofed headers grant nothing, read-only cannot write). service-settings suite 144 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 101 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…settings Enforcing the settings manifests' declared writePermission (previous commit) surfaced a modeling gap: `setup.write` — the write counterpart to `setup.access`, used by the branding/company/localization/feature-flag manifests — was referenced but NEVER declared as a capability or granted to any permission set. Harmless while settings writes went ungated; under enforcement it meant NOBODY (not even an admin) could write those namespaces (403). - Declare `setup.write` in PLATFORM_CAPABILITIES (scope: org). - Grant it to admin_full_access and organization_admin (the sets that already hold setup.access / manage tenant settings). Verified: the analytics-timezone dogfood (which PUTs /api/settings/ localization as the signed-in admin, then buckets) now passes — proving the admin bearer resolves through the settings plugin's verified resolution and the write gate admits it. plugin-security 298, service-settings 144, spec api-surface unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jul 12, 2026
The raw-app share-link routes trusted x-user-id/x-tenant-id headers and the service ignored the caller on revoke — so a client could forge link attribution, enumerate another user's link tokens (?createdBy=<victim>, which then resolve records under a system context bypassing RLS), and revoke arbitrary users' links. - Verified identity: SharingServicePlugin derives the caller (+ their positions/permissions) from resolveAuthzContext (session/API key/OAuth), never headers. Route default is SECURE (anonymous). Create/list/revoke require a signed-in principal (401); the public /:token/resolve stays public but keys its audience:'signed_in' check off the verified session, not a spoofable x-user-id. - List scoping: GET /share-links is forced to the caller's own links — no more ?createdBy=<victim> enumeration. - Revoke ownership: revokeLink requires the caller to be the creator (system callers bypass); previously the context was ignored. - Create access check: createLink verifies the record is visible to the caller (read under the caller's RLS) before minting — you can only share a record you can see. ShareLinkExecutionContext gains optional positions/permissions for the record-access check. Companion to #2848 (settings routes). Tests: revoke-ownership (non-owner denied, creator + system allowed), create-access (untrusted caller → 403 for an unseen record, system → 404). plugin-sharing 79 green; spec api-surface unchanged; tsc clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 12, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Jul 13, 2026
14.5.0 has shipped (all @objectstack/* packages version-bumped, per-package changelogs generated), but the curated platform page still read "current: 14.4.0" and covered only 14.1 → 14.4. Per docs/releases-maintenance.md a minor folds into the current major's page rather than getting its own. - Bump the "current" marker to 14.5.0 (July 13, 2026) and the covered range to 14.1 → 14.5, in both v14.mdx and the releases index. - New capabilities: MCP agents run under a scope-derived ceiling (ADR-0090 D10) and the ADR-0057 data-lifecycle follow-ups (retention sweepers retired, retention.onlyWhen, telemetry datasource split, Studio lifecycle block). - Notable fixes: the settings-routes (Critical, #2848), share-link-routes (#2851), and analytics-scope (#2852) verified-authz hardenings. - Console: extend the bundled objectui range to 839536b1 and add the Edit-as-CTA / inline-edit and permission-sets (Studio designs, Setup assigns) frontend work. - Upgrade checklist: retention-option removal, MCP OAuth scope ceilings, and the settings/share-link verified-credential requirement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CxYGfsQTH35x4nBBjKkVvp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Severity: Critical (unauthenticated remote write of platform configuration)
Found by an adversarial security review of the request→
ExecutionContexttrust boundary (during the ADR-0090 D10 agent-security review). Not related to the D10 agent work — a pre-existing issue in@objectstack/service-settings.The hole
The settings HTTP routes (
GET/PUT/POST /api/settings/*) derived the caller's identity fromx-user-id/x-tenant-id/x-permissionsrequest headers (the route defaultdefaultContext), and the write pathsetManyperformed no permission check at all. The routes are registered on the raw Hono app (IHttpServer) with no auth middleware, andos serve --servercomposes the settings service + HTTP server by default.Result: an unauthenticated remote client could
PUT /api/settings/<namespace>(withx-tenant-id: <victim>or a global-scoped key) → write tenant- or platform-scoped settings, including the auth security-policy, localization, and company manifests;GET /api/settings→ enumerate every namespace (theperms.size === 0 ⇒ return allpass-through);x-user-id.The fix
SettingsServicePluginnow derives identity + capabilities from the platform's verified resolution —resolveAuthzContext(session cookie / API key / OAuth) — never from request headers. The route default is now secure: it trusts no identity header and yields an anonymous, denied context. A custom integration may still inject its owncontextFromRequest.readPermission/writePermissionare enforced for HTTP callers: reads of a protected namespace, writes, and actions require the declared capability (writes default to at least the read capability — never ungated). Enforced behind a newenforcedflag set only at the HTTP boundary, so in-process / boot callers (kernel.getService('settings'), seeders, and the email/auth/storage/sms plugins that read their namespace) are unchanged and keep full trusted access.403 SETTINGS_FORBIDDEN.Behaviour change
A deployment that relied on the old header-trusted default must present a real verified session / API-key / OAuth credential — which the console already does (cookie/token flows through
resolveAuthzContext). ThecontextFromRequestoption remains the escape hatch for bespoke gateways.Tests
settings-service.test.ts): enforced read/write/action gates; read-only capability cannot write; trusted (non-enforced) in-process callers bypass — pass-through preserved.settings-routes.test.ts): anonymous read hides protected manifests; anonymous write → 403; spoofedx-user-id/x-permissionsheaders grant nothing; a read-only caller may read but not write.@objectstack/service-settingssuite 144 green.🤖 Generated with Claude Code