Skip to content

fix(settings): verified authz on settings routes — close unauthenticated write (Critical)#2848

Merged
os-zhuang merged 2 commits into
mainfrom
fix/settings-routes-verified-authz
Jul 12, 2026
Merged

fix(settings): verified authz on settings routes — close unauthenticated write (Critical)#2848
os-zhuang merged 2 commits into
mainfrom
fix/settings-routes-verified-authz

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Severity: Critical (unauthenticated remote write of platform configuration)

Found by an adversarial security review of the request→ExecutionContext trust 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 from x-user-id / x-tenant-id / x-permissions request headers (the route default defaultContext), and the write path setMany performed no permission check at all. The routes are registered on the raw Hono app (IHttpServer) with no auth middleware, and os serve --server composes the settings service + HTTP server by default.

Result: an unauthenticated remote client could

  • PUT /api/settings/<namespace> (with x-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/settingsenumerate every namespace (the perms.size === 0 ⇒ return all pass-through);
  • forge attribution via x-user-id.

The fix

  • Verified identity. SettingsServicePlugin now 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 own contextFromRequest.
  • Capability gates. The manifest's declared readPermission / writePermission are 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 new enforced flag 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.
  • Unauthenticated HTTP → 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). The contextFromRequest option remains the escape hatch for bespoke gateways.

Tests

  • Service-level (settings-service.test.ts): enforced read/write/action gates; read-only capability cannot write; trusted (non-enforced) in-process callers bypass — pass-through preserved.
  • Route-level (settings-routes.test.ts): anonymous read hides protected manifests; anonymous write → 403; spoofed x-user-id/x-permissions headers grant nothing; a read-only caller may read but not write.
  • @objectstack/service-settings suite 144 green.

🤖 Generated with Claude Code

… (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>
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 12, 2026 9:05am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 12, 2026
@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-security, packages/services, @objectstack/spec.

101 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
os-zhuang merged commit c044f08 into main Jul 12, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the fix/settings-routes-verified-authz branch July 12, 2026 09:17
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>
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant