Skip to content

fix(security): trust-access oracle + proxied-action org checks (GH-042 follow-ups) - #3578

Draft
dennisofficial wants to merge 1 commit into
mainfrom
fix/gh-followups-trust-access
Draft

dennisofficial wants to merge 1 commit into
mainfrom
fix/gh-followups-trust-access

Conversation

@dennisofficial

@dennisofficial dennisofficial commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up fixes from the security review of #3573 (merged). All changes are minimal, in-place fixes.

  • Trust-access sibling oracle (completes Feature Request: Trust Portal #42) — POST /v1/trust-access/:org/requests returned three distinguishable responses: already_approved with grant.expiresAt for an active grant, a 400 for a pending request, and a 201 for a new request. All three paths now return one generic message — the same shape the merged reclaim fix uses — and grant details are never in the response body. The fresh access link still goes out by email on the already-approved path.
  • Enterprise-proxied server actions (feat: editor switch to liveblocks #52 residual) — uploadAutomationScript, executeAutomationScript, publishAutomation, and restoreVersion verified the caller's session org but not that the forwarded taskId/automationId belongs to that org. Each now checks ownership before proxying (automation actions via the existing isAutomationInOrganization helper; upload verifies the task's organization directly since it takes no automationId).
  • CreateVersionDto.scriptKey prefix check — AutomationsService.createVersion now rejects script keys outside the caller's ${organizationId}/ S3 prefix (including .. escapes), so a caller with task-update permission cannot register another tenant's object as an automation version.
  • Status filters on public trust readers (no toast or invite to portal #272 residual) — six unauthenticated readers resolved trust portals without a status filter: getPublicOverview, getPublicCustomLinks, getPublicVendors, getPublicFavicon, getPublicSecurityQuestionnaireEnabled (via resolveTrustByFriendlyUrl), and getPublicCustomFrameworks (via resolveOrganizationId). All now resolve published portals only; a draft portal behaves exactly like a missing one.
  • ensureFriendlyUrl published literal dropped — the trust-access ensureFriendlyUrl upsert no longer sets status: 'published' on create.

Verification

  • apps/api: NODE_ENV=test bunx jest src/tasks/automations src/trust-portal — 166 tests pass, including new specs asserting identical generic responses across all three createAccessRequest paths (no id/status/grant leakage), published-only resolution for each public reader, and the scriptKey prefix/.. rejection. Two controller suites fail at module load with a pre-existing DATABASE_URL/TLS guard error — reproduced identically at origin/main, unrelated to this diff.
  • apps/app: bunx vitest run src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.test.ts — 10/10 pass (new spec: cross-org task/automation rejection, missing records, and success paths for all four actions).
  • Typecheck: tsc --noEmit output for apps/api and apps/app is byte-identical to the origin/main baseline (pre-existing errors only, none in touched files).
  • Lint: changed app files are eslint-clean; changed API files carry only the pre-existing unbound-method error class already present at origin/main on these spec files (7 baseline, +2 from new tests using the same assertion pattern; lint is not CI-gated).

Things to be aware of

  1. Trust.status schema default is @default(published) (packages/db/prisma/schema/trust.prisma). Dropping the literal from ensureFriendlyUrl removes the explicit publish-on-create, but a row created there would still default to published. Fully closing that path needs a schema default change to draft — a product decision, since existing behavior elsewhere relies on the default. Not changed here.
  2. getAutomationRunStatus residual gap from fix: P0 auth and tenant-scoping security fixes (GH-036, GH-042, GH-046, GH-049, GH-052, GH-075, GH-102, GH-272) #3573 still stands: enterprise/trigger run ids have no org mapping in this app's database, so that action can only require an authenticated session. Closing it needs the enterprise service to scope runs by org.
  3. Response-shape change on a public endpoint — POST /v1/trust-access/:org/requests no longer returns the created request's id/status. No consumer in this repo reads those fields; if an external portal frontend does, it should rely on the email flow instead.
  4. Rate limiting on the requests/reclaim endpoints remains unimplemented, as in fix: P0 auth and tenant-scoping security fixes (GH-036, GH-042, GH-046, GH-049, GH-052, GH-075, GH-102, GH-272) #3573 — the generic responses remove the enumeration signal, but per-IP/org throttling is still worth adding.

Summary by cubic

Closes the remaining security gaps from the GH-42 review: trust-access request responses no longer leak grant details, and proxied automation actions are now scope-checked before forwarding.

Security fixes

  • POST /v1/trust-access/:org/requests returns the same generic message on all three paths (active grant, pending, new) and never includes grant details; the fresh link still goes out by email.
  • uploadAutomationScript, executeAutomationScript, publishAutomation, and restoreVersion verify the forwarded task/automation belongs to the caller's org before proxying to the enterprise API.
  • AutomationsService.createVersion rejects scriptKey values outside the caller's ${organizationId}/ S3 prefix, including .. escapes.
  • Six unauthenticated trust readers (getPublicOverview, getPublicCustomLinks, getPublicVendors, getPublicFavicon, getPublicSecurityQuestionnaireEnabled, getPublicCustomFrameworks) now resolve only published portals; a draft behaves like a missing one.

Behavior changes

  • The createAccessRequest endpoint no longer returns the created request's id or status; consumers should rely on the email flow.
  • Dropped the status: 'published' literal from the trust-access ensureFriendlyUrl upsert, though the schema default remains published.

Written for commit 92966a5. Summary will update on new commits.

Review in cubic

…follow-ups)

- POST /v1/trust-access/:org/requests returned three distinguishable
  responses (already-approved with grant.expiresAt, 400 for a pending
  request, 201 for a new one) — an email-enumeration oracle. All paths
  now return one generic message, mirroring the reclaimAccess fix, and
  grant details are never in the body.
- uploadAutomationScript, executeAutomationScript, publishAutomation and
  restoreVersion checked the session org but not that the forwarded
  taskId/automationId belongs to it; each now verifies ownership before
  proxying to the enterprise API.
- createVersion now rejects scriptKey values outside the caller's
  organization S3 prefix (including '..' escapes), so a caller cannot
  register another tenant's object as an automation version.
- Six public trust readers resolved portals without a status filter;
  all now only resolve published portals (overview, custom links,
  vendors, favicon, security questionnaire, custom frameworks).
- Dropped the status: 'published' literal from the trust-access
  ensureFriendlyUrl upsert.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@dennisofficial

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@dennisofficial I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files

Confidence score: 2/5

  • In task-automation-actions.ts, org ownership alone allows any authenticated org member to reach restore/publish mutations without task-update permission; guard these mutations through an API endpoint that checks the required permission.
  • In task-automation-actions.ts, the forwarded taskId is not verified against the automation, so a caller can proxy an in-org automation with a different task ID; validate that the task and automation are bound before proxying.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts">

<violation number="1" location="apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts:269">
P1: Bind the forwarded `taskId` to the automation before proxying; this check proves only that the automation belongs to the org, so a caller can submit an in-org automation with another task ID and still reach the enterprise action.</violation>

<violation number="2" location="apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts:273">
P1: Any authenticated member of the active org can reach the restore/publish proxy without task-update permission because this check only verifies org ownership. Route these mutations through an API endpoint guarded by `PermissionGuard` and `@RequirePermission('task', 'update')` before proxying.

(Based on your team's feedback about RBAC permission checks.)</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

automationId: data.automationId,
organizationId: activeOrganizationId,
});
if (!belongsToOrg) {

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Any authenticated member of the active org can reach the restore/publish proxy without task-update permission because this check only verifies org ownership. Route these mutations through an API endpoint guarded by PermissionGuard and @RequirePermission('task', 'update') before proxying.

(Based on your team's feedback about RBAC permission checks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts, line 273:

<comment>Any authenticated member of the active org can reach the restore/publish proxy without task-update permission because this check only verifies org ownership. Route these mutations through an API endpoint guarded by `PermissionGuard` and `@RequirePermission('task', 'update')` before proxying.

(Based on your team's feedback about RBAC permission checks.) </comment>

<file context>
@@ -255,6 +266,14 @@ export async function executeAutomationScript(data: {
+      automationId: data.automationId,
+      organizationId: activeOrganizationId,
+    });
+    if (!belongsToOrg) {
+      return { success: false, error: 'Unauthorized' };
+    }
</file context>
Fix with cubic

return { success: false, error: 'Unauthorized' };
}

const belongsToOrg = await isAutomationInOrganization({

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Bind the forwarded taskId to the automation before proxying; this check proves only that the automation belongs to the org, so a caller can submit an in-org automation with another task ID and still reach the enterprise action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts, line 269:

<comment>Bind the forwarded `taskId` to the automation before proxying; this check proves only that the automation belongs to the org, so a caller can submit an in-org automation with another task ID and still reach the enterprise action.</comment>

<file context>
@@ -255,6 +266,14 @@ export async function executeAutomationScript(data: {
       return { success: false, error: 'Unauthorized' };
     }
 
+    const belongsToOrg = await isAutomationInOrganization({
+      automationId: data.automationId,
+      organizationId: activeOrganizationId,
</file context>
Fix with cubic

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants