diff --git a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts index 39e56e055..dadfc7d43 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-fns.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-fns.ts @@ -146,6 +146,18 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) }; export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => { + // Check if provided service account email prefix matches organization ID. + // We do this to mitigate confused deputy attacks in multi-tenant instances + const expectedEmailPrefix = appConnection.orgId.split("-").slice(0, 2).join("-"); + if ( + appConnection.credentials.serviceAccountEmail && + !appConnection.credentials.serviceAccountEmail.startsWith(expectedEmailPrefix) + ) { + throw new BadRequestError({ + message: `GCP service account email must have a prefix of "${expectedEmailPrefix}"` + }); + } + await getAuthToken(appConnection); return appConnection.credentials; diff --git a/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts b/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts index 5ad919adc..3c313f205 100644 --- a/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts +++ b/backend/src/services/app-connection/gcp/gcp-connection-schemas.ts @@ -11,7 +11,7 @@ import { import { GcpConnectionMethod } from "./gcp-connection-enums"; export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({ - serviceAccountEmail: z.string().trim().min(1, "Service account email required") + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") }); const BaseGcpConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GCP) }); diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx index d647c29a6..bdcb96242 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GcpConnectionForm.tsx @@ -10,6 +10,7 @@ import { Select, SelectItem } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { GcpConnectionMethod, TGcpConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -32,7 +33,7 @@ const formSchema = z.discriminatedUnion("method", [ rootSchema.extend({ method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation), credentials: z.object({ - serviceAccountEmail: z.string().trim().min(1, "Service account email required") + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") }) }) ]); @@ -49,6 +50,7 @@ export const GcpConnectionForm = ({ appConnection, onSubmit }: Props) => { method: GcpConnectionMethod.ServiceAccountImpersonation } }); + const { currentOrg } = useOrganization(); const { handleSubmit, @@ -101,6 +103,7 @@ export const GcpConnectionForm = ({ appConnection, onSubmit }: Props) => { isError={Boolean(error?.message)} label="Service Account Email" className="group" + helperText={`Service account email must be prefixed with "${currentOrg.id.split("-").slice(0, 2).join("-")}".`} >