-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: security release 2026-07-21 #4528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1de894a
fix(webapp): scope org settings routes to caller membership (#85)
1stvamp 5e22346
Merge remote-tracking branch 'upstream/main' into release/2026-07-21
0eca444
fix(rbac): deny a non-member session in an org context (OSS fallback)…
1stvamp da8bb31
fix(webapp): block benchmarking IP range for alert webhooks (#84)
carderne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| Reject alert webhook destinations in reserved benchmarking IP ranges. |
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { postgresTest } from "@internal/testcontainers"; | ||
| import plugin from "@trigger.dev/rbac"; | ||
| import { type PrismaClient } from "@trigger.dev/database"; | ||
| import { describe, expect, vi } from "vitest"; | ||
| import { | ||
| createTestOrgProjectWithMember, | ||
| createTestUser, | ||
| } from "./fixtures/environmentVariablesFixtures"; | ||
|
|
||
| vi.setConfig({ testTimeout: 60_000 }); | ||
|
|
||
| // The RBAC fallback ability is permissive (`can: () => true` for a non-admin), so | ||
| // `ability.can` is not a tenant floor. `authenticateSession` is the gate every | ||
| // org-scoped dashboard route relies on; a non-member in an org context must be | ||
| // denied here, or a permissive ability lets them act on any org whose slug they | ||
| // know. The route-level e2e (auth-dashboard.e2e.full) covers the HTTP path; this | ||
| // pins the fallback gate directly since that path can't run without a container. | ||
| function fallback(prisma: PrismaClient) { | ||
| // forceFallback skips the closed-source plugin and uses the in-repo fallback. | ||
| return plugin.create({ primary: prisma, replica: prisma }, { forceFallback: true }); | ||
| } | ||
|
|
||
| const request = new Request("https://app.trigger.dev/orgs/x/settings/roles"); | ||
|
|
||
| describe("RBAC fallback authenticateSession — org membership floor", () => { | ||
| postgresTest("denies a non-member in an org context", async ({ prisma }) => { | ||
| const { organization } = await createTestOrgProjectWithMember(prisma); | ||
| const outsider = await createTestUser(prisma); | ||
|
|
||
| const result = await fallback(prisma).authenticateSession(request, { | ||
| userId: outsider.id, | ||
| organizationId: organization.id, | ||
| }); | ||
|
|
||
| expect(result).toMatchObject({ ok: false, reason: "unauthorized" }); | ||
| }); | ||
|
|
||
| postgresTest("allows a member in an org context", async ({ prisma }) => { | ||
| const { user, organization } = await createTestOrgProjectWithMember(prisma); | ||
|
|
||
| const result = await fallback(prisma).authenticateSession(request, { | ||
| userId: user.id, | ||
| organizationId: organization.id, | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| }); | ||
|
|
||
| postgresTest( | ||
| "stays permissive with no org context, even for a non-member", | ||
| async ({ prisma }) => { | ||
| // Identity-only checks (no organizationId) predate any scope, so the floor | ||
| // does not apply and the permissive baseline is preserved. | ||
| const outsider = await createTestUser(prisma); | ||
|
|
||
| const result = await fallback(prisma).authenticateSession(request, { userId: outsider.id }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| } | ||
| ); | ||
|
|
||
| // A project-only scope is still a tenant claim, so the floor resolves the | ||
| // project's organization rather than letting the context through unchecked. | ||
| postgresTest("denies a non-member scoped only to a project", async ({ prisma }) => { | ||
| const { project } = await createTestOrgProjectWithMember(prisma); | ||
| const outsider = await createTestUser(prisma); | ||
|
|
||
| const result = await fallback(prisma).authenticateSession(request, { | ||
| userId: outsider.id, | ||
| projectId: project.id, | ||
| }); | ||
|
|
||
| expect(result).toMatchObject({ ok: false, reason: "unauthorized" }); | ||
| }); | ||
|
|
||
| postgresTest("allows a member scoped only to a project", async ({ prisma }) => { | ||
| const { user, project } = await createTestOrgProjectWithMember(prisma); | ||
|
|
||
| const result = await fallback(prisma).authenticateSession(request, { | ||
| userId: user.id, | ||
| projectId: project.id, | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| }); | ||
|
|
||
| // The membership probe reads the replica first and the primary on a miss, so a | ||
| // member whose row has not replicated yet is not bounced. Modelled by giving | ||
| // the controller a replica that cannot see the row and a primary that can. | ||
| postgresTest("allows a member the replica has not caught up on", async ({ prisma }) => { | ||
| const { user, organization } = await createTestOrgProjectWithMember(prisma); | ||
| const blindReplica = { | ||
| ...prisma, | ||
| orgMember: { findFirst: async () => null }, | ||
| user: prisma.user, | ||
| project: prisma.project, | ||
| } as unknown as PrismaClient; | ||
|
carderne marked this conversation as resolved.
|
||
|
|
||
| const controller = plugin.create( | ||
| { primary: prisma, replica: blindReplica }, | ||
| { forceFallback: true } | ||
| ); | ||
| const result = await controller.authenticateSession(request, { | ||
| userId: user.id, | ||
| organizationId: organization.id, | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| }); | ||
| }); | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { postgresTest } from "@internal/testcontainers"; | ||
| import { describe, expect, vi } from "vitest"; | ||
|
|
||
| vi.setConfig({ testTimeout: 60_000 }); | ||
| import { resolveOrgIdFromSlug, resolveOrgIdFromSlugForUser } from "~/models/organization.server"; | ||
| import { | ||
| createTestOrgProjectWithMember, | ||
| createTestUser, | ||
| } from "./fixtures/environmentVariablesFixtures"; | ||
|
|
||
| // The org settings routes resolve their org through this helper, so a non-member resolving to null | ||
| // is what makes the dashboard route builder fail closed. ability.can is not a tenant floor (the RBAC | ||
| // plugin and the OSS fallback both return a permissive ability for a non-member), so without the | ||
| // membership filter a non-member reached those routes for any org whose slug they knew: a live | ||
| // cross-tenant read of SSO/directory-sync config and an open set-role gate, confirmed on test-cloud. | ||
| describe("resolveOrgIdFromSlugForUser", () => { | ||
| postgresTest("resolves an org the user is a member of", async ({ prisma }) => { | ||
| const { user, organization } = await createTestOrgProjectWithMember(prisma); | ||
|
|
||
| const resolved = await resolveOrgIdFromSlugForUser(organization.slug, user.id, prisma, prisma); | ||
|
|
||
| expect(resolved).toBe(organization.id); | ||
| }); | ||
|
|
||
| postgresTest("returns null for a non-member, the tenant floor", async ({ prisma }) => { | ||
| const { organization: target } = await createTestOrgProjectWithMember(prisma); | ||
| const outsider = await createTestUser(prisma); | ||
|
|
||
| const resolved = await resolveOrgIdFromSlugForUser(target.slug, outsider.id, prisma, prisma); | ||
|
|
||
| // The unscoped resolver still hands the same non-member the org id: this is the exact gap the | ||
| // membership filter closes, and why scoping by slug alone was the hole. | ||
| const unscoped = await resolveOrgIdFromSlug(target.slug, prisma, prisma); | ||
| expect(unscoped).toBe(target.id); | ||
| expect(resolved).toBeNull(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.