Skip to content

Commit 5a5833e

Browse files
authored
fix(webapp): scope deployment lookup by environment to prevent cross-env cancel IDOR (#70)
* fix(webapp): scope deployment lookup by environment to prevent cross-env cancel IDOR The private getDeployment helper in deployment.server.ts resolved worker deployments scoped only by projectId. Because a single API key authenticates to one environment but projectId is shared across all environments of a project, a lower-trust environment key (dev/preview/CI) could cancel, progress, or request registry credentials for another environment's (e.g. prod) in-progress deployment if it knew the target's friendlyId. Scope getDeployment by environmentId instead of projectId, and update the three call sites (cancelDeployment, progressDeployment, generateRegistryCredentials) to pass the environment id. The registry-credentials platform call stays project+region scoped by design. The dashboard cancel route (resources.$projectId.deployments.$deploymentShortCode.cancel) is project-scoped by design via RBAC; it now passes the located deployment's own environmentId so its behavior is preserved. GHSA-4672-hwv6-gq62 * fix(webapp): remove unused project ID from deployment cancellation * chore(webapp): format deployment cancellation call
1 parent 7fd671a commit 5a5833e

4 files changed

Lines changed: 11 additions & 14 deletions

File tree

apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
4545
const deploymentService = new DeploymentService();
4646

4747
return await deploymentService
48-
.cancelDeployment(authenticatedEnv, deploymentId, {
48+
.cancelDeployment({ id: authenticatedEnv.id }, deploymentId, {
4949
canceledReason: body.data.reason,
5050
})
5151
.match(

apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.cancel.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const action = dashboardAction(
7575
prisma.workerDeployment.findUnique({
7676
select: {
7777
friendlyId: true,
78-
projectId: true,
78+
environmentId: true,
7979
},
8080
where: {
8181
projectId_shortCode: {
@@ -96,10 +96,7 @@ export const action = dashboardAction(
9696
const result = await verifyProjectMembership()
9797
.andThen(findDeploymentFriendlyId)
9898
.andThen((deployment) =>
99-
deploymentService.cancelDeployment(
100-
{ projectId: deployment.projectId },
101-
deployment.friendlyId
102-
)
99+
deploymentService.cancelDeployment({ id: deployment.environmentId }, deployment.friendlyId)
103100
);
104101

105102
if (result.isErr()) {

apps/webapp/app/v3/services/deployment.server.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export class DeploymentService extends BaseService {
169169
})
170170
);
171171

172-
return this.getDeployment(authenticatedEnv.projectId, friendlyId)
172+
return this.getDeployment(authenticatedEnv.id, friendlyId)
173173
.andThen(validateDeployment)
174174
.andThen((deployment) => {
175175
if (deployment.status === "PENDING") {
@@ -191,7 +191,7 @@ export class DeploymentService extends BaseService {
191191
* @param data Cancelation reason.
192192
*/
193193
public cancelDeployment(
194-
authenticatedEnv: Pick<AuthenticatedEnvironment, "projectId">,
194+
authenticatedEnv: Pick<AuthenticatedEnvironment, "id">,
195195
friendlyId: string,
196196
data?: Partial<Pick<WorkerDeployment, "canceledReason">>
197197
) {
@@ -246,7 +246,7 @@ export class DeploymentService extends BaseService {
246246
cause: error,
247247
}));
248248

249-
return this.getDeployment(authenticatedEnv.projectId, friendlyId)
249+
return this.getDeployment(authenticatedEnv.id, friendlyId)
250250
.andThen(validateDeployment)
251251
.andThen(cancelDeployment)
252252
.andThen(({ deployment }) =>
@@ -278,7 +278,7 @@ export class DeploymentService extends BaseService {
278278
* @param friendlyId The friendly deployment ID.
279279
*/
280280
public generateRegistryCredentials(
281-
authenticatedEnv: Pick<AuthenticatedEnvironment, "projectId">,
281+
authenticatedEnv: Pick<AuthenticatedEnvironment, "id" | "projectId">,
282282
friendlyId: string
283283
) {
284284
const validateDeployment = (
@@ -326,7 +326,7 @@ export class DeploymentService extends BaseService {
326326
});
327327
});
328328

329-
return this.getDeployment(authenticatedEnv.projectId, friendlyId)
329+
return this.getDeployment(authenticatedEnv.id, friendlyId)
330330
.andThen(validateDeployment)
331331
.andThen(getDeploymentRegion)
332332
.andThen(generateCredentials);
@@ -472,12 +472,12 @@ export class DeploymentService extends BaseService {
472472
);
473473
}
474474

475-
private getDeployment(projectId: string, friendlyId: string) {
475+
private getDeployment(environmentId: string, friendlyId: string) {
476476
return fromPromise(
477477
this._prisma.workerDeployment.findFirst({
478478
where: {
479479
friendlyId,
480-
projectId,
480+
environmentId,
481481
},
482482
select: {
483483
status: true,

apps/webapp/app/v3/services/initializeDeployment.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export class InitializeDeploymentService extends BaseService {
286286
});
287287

288288
return deploymentService
289-
.cancelDeployment(environment, deployment.friendlyId, {
289+
.cancelDeployment({ id: environment.id }, deployment.friendlyId, {
290290
canceledReason: "Failed to enqueue build, please try again shortly.",
291291
})
292292
.orTee((cancelError) =>

0 commit comments

Comments
 (0)