diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts index 3bd409faf..57c05b6fb 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -1,31 +1,51 @@ -import crypto from "node:crypto"; +import dns from "node:dns/promises"; +import net from "node:net"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; +import { isPrivateIp } from "@app/lib/ip/ipRange"; import { getDbConnectionHost } from "@app/lib/knex"; -export const verifyHostInputValidity = (host: string, isGateway = false) => { +export const verifyHostInputValidity = async (host: string, isGateway = false) => { const appCfg = getConfig(); - const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI); - // no need for validation when it's dev - if (appCfg.NODE_ENV === "development") return; + // if (appCfg.NODE_ENV === "development") return; // incase you want to remove this check in dev - if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" }); + const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat( + (appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)), + getDbConnectionHost(appCfg.REDIS_URL) + ); - if ( - appCfg.isCloud && - !isGateway && - // localhost - // internal ips - (host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/)) - ) - throw new BadRequestError({ message: "Invalid db host" }); - - if ( - host === "localhost" || - host === "127.0.0.1" || - (dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host))) - ) { - throw new BadRequestError({ message: "Invalid db host" }); + // get host db ip + const exclusiveIps: string[] = []; + for await (const el of reservedHosts) { + if (el) { + if (net.isIPv4(el)) { + exclusiveIps.push(el); + } else { + const resolvedIps = await dns.resolve4(el); + exclusiveIps.push(...resolvedIps); + } + } } + + const normalizedHost = host.split(":")[0]; + const inputHostIps: string[] = []; + if (net.isIPv4(host)) { + inputHostIps.push(host); + } else { + if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") { + throw new BadRequestError({ message: "Invalid db host" }); + } + const resolvedIps = await dns.resolve4(host); + inputHostIps.push(...resolvedIps); + } + + if (!isGateway) { + const isInternalIp = inputHostIps.some((el) => isPrivateIp(el)); + if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" }); + } + + const isAppUsedIps = inputHostIps.some((el) => exclusiveIps.includes(el)); + if (isAppUsedIps) throw new BadRequestError({ message: "Invalid db host" }); + return inputHostIps; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index 534a5c8b2..f2907f7dc 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -13,6 +13,7 @@ import { customAlphabet } from "nanoid"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; @@ -144,6 +145,14 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { // We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements. CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement)); DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement)); + validateHandlebarTemplate("AWS ElastiCache creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.revocationStatement) { + validateHandlebarTemplate("AWS ElastiCache revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + } return providerInputs; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index b2f1f8c35..5e4250a35 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -3,9 +3,10 @@ import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; import { z } from "zod"; -import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models"; const generatePassword = (size = 48) => { @@ -20,11 +21,20 @@ const generateUsername = () => { export const CassandraProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs); - if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") { - throw new BadRequestError({ message: "Invalid db host" }); + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("Cassandra creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration", "keyspace"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("Cassandra renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration", "keyspace"].includes(val) + }); } + validateHandlebarTemplate("Cassandra revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); - return providerInputs; + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index e91363629..e3ad9c112 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -19,9 +19,8 @@ const generateUsername = () => { export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index 84dec4d68..42fea268d 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -19,8 +19,8 @@ const generateUsername = () => { export const MongoDBProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index 32264ecfa..a259c6bde 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -79,9 +79,8 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa export const RabbitMqProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 306b8c59c..78efe98e3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models"; @@ -51,8 +52,20 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi export const RedisDatabaseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("Redis creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("Redis renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration"].includes(val) + }); + } + validateHandlebarTemplate("Redis revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts index f349d36bd..37e0eb36c 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models"; @@ -27,8 +28,16 @@ export const SapAseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password"].includes(val) + }); + if (providerInputs.revocationStatement) { + validateHandlebarTemplate("SAP ASE revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + } + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer, useMaster?: boolean) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index ecd7ba3d3..7ef114c5f 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -11,6 +11,7 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models"; @@ -28,8 +29,19 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); - return providerInputs; + const [hostIp] = await verifyHostInputValidity(providerInputs.host); + validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("SAP Hana renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration"].includes(val) + }); + } + validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts index 3550b146d..bea7eca89 100644 --- a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts +++ b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models"; @@ -31,6 +32,18 @@ const getDaysToExpiry = (expiryDate: Date) => { export const SnowflakeProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs); + validateHandlebarTemplate("Snowflake creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("Snowflake renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration"].includes(val) + }); + } + validateHandlebarTemplate("Snowflake revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username"].includes(val) + }); + return providerInputs; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index eea9fef94..852a844ce 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; @@ -117,8 +118,21 @@ type TSqlDatabaseProviderDTO = { export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); - return providerInputs; + + const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); + validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { + allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) + }); + if (providerInputs.renewStatement) { + validateHandlebarTemplate("SQL renew", providerInputs.renewStatement, { + allowedExpressions: (val) => ["username", "expiration", "database"].includes(val) + }); + } + validateHandlebarTemplate("SQL revoke", providerInputs.revocationStatement, { + allowedExpressions: (val) => ["username", "database"].includes(val) + }); + + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { @@ -144,7 +158,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) } : undefined }, - acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT + acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT, + pool: { min: 0, max: 7 } }); return db; }; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 0e8a7fbf6..cc66411ee 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -5,6 +5,7 @@ import { ActionProjectType, TableName } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -86,6 +87,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ message: "Failed to update more privileged identity", details: { missingPermissions: permissionBoundary.missingPermissions } }); + validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -173,6 +177,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ details: { missingPermissions: permissionBoundary.missingPermissions } }); + validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + if (data?.slug) { const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ slug: data.slug, diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 4b259ef55..9cda292f6 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -5,6 +5,7 @@ import { ActionProjectType } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -102,6 +103,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ @@ -203,6 +208,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; + validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; if (isTemporary) { diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 58a2f892b..07bc7d4d3 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -5,6 +5,7 @@ import { ActionProjectType, TableName } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; @@ -92,6 +93,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (existingSlug) throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); + validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ @@ -185,6 +190,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` }); } + validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions)); diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts index 46c519d58..e86469c51 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts @@ -8,10 +8,9 @@ import axios from "axios"; import jmespath from "jmespath"; import knex from "knex"; -import { getConfig } from "@app/lib/config/env"; -import { getDbConnectionHost } from "@app/lib/knex"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns"; import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types"; import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types"; @@ -88,32 +87,14 @@ export const secretRotationDbFn = async ({ variables, options }: TSecretRotationDbFn) => { - const appCfg = getConfig(); - const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; - const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not - const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI); - - if ( - isCloud && - // internal ips - (host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/)) - ) - throw new Error("Invalid db host"); - if ( - host === "localhost" || - host === "127.0.0.1" || - // database infisical uses - dbHost === host - ) - throw new Error("Invalid db host"); - + const [hostIp] = await verifyHostInputValidity(host); const db = knex({ client, connection: { database, port, - host, + host: hostIp, user: username, password, connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, diff --git a/backend/src/lib/ip/ipRange.ts b/backend/src/lib/ip/ipRange.ts new file mode 100644 index 000000000..c2a77d6f9 --- /dev/null +++ b/backend/src/lib/ip/ipRange.ts @@ -0,0 +1,61 @@ +import { BlockList } from "node:net"; + +import { BadRequestError } from "../errors"; +// Define BlockList instances for each range type +const ipv4RangeLists: Record = { + unspecified: new BlockList(), + broadcast: new BlockList(), + multicast: new BlockList(), + linkLocal: new BlockList(), + loopback: new BlockList(), + carrierGradeNat: new BlockList(), + private: new BlockList(), + reserved: new BlockList() +}; + +// Add IPv4 CIDR ranges to each BlockList +ipv4RangeLists.unspecified.addSubnet("0.0.0.0", 8); +ipv4RangeLists.broadcast.addAddress("255.255.255.255"); +ipv4RangeLists.multicast.addSubnet("224.0.0.0", 4); +ipv4RangeLists.linkLocal.addSubnet("169.254.0.0", 16); +ipv4RangeLists.loopback.addSubnet("127.0.0.0", 8); +ipv4RangeLists.carrierGradeNat.addSubnet("100.64.0.0", 10); + +// IPv4 Private ranges +ipv4RangeLists.private.addSubnet("10.0.0.0", 8); +ipv4RangeLists.private.addSubnet("172.16.0.0", 12); +ipv4RangeLists.private.addSubnet("192.168.0.0", 16); + +// IPv4 Reserved ranges +ipv4RangeLists.reserved.addSubnet("192.0.0.0", 24); +ipv4RangeLists.reserved.addSubnet("192.0.2.0", 24); +ipv4RangeLists.reserved.addSubnet("192.88.99.0", 24); +ipv4RangeLists.reserved.addSubnet("198.18.0.0", 15); +ipv4RangeLists.reserved.addSubnet("198.51.100.0", 24); +ipv4RangeLists.reserved.addSubnet("203.0.113.0", 24); +ipv4RangeLists.reserved.addSubnet("240.0.0.0", 4); + +/** + * Checks if an IP address (IPv4) is private or public + * inspired by: https://github.com/whitequark/ipaddr.js/blob/main/lib/ipaddr.js + */ +export const getIpRange = (ip: string): string => { + try { + const rangeLists = ipv4RangeLists; + // Check each range type + for (const rangeName in rangeLists) { + if (Object.hasOwn(rangeLists, rangeName)) { + if (rangeLists[rangeName].check(ip)) { + return rangeName; + } + } + } + + // If no range matched, it's a public address + return "unicast"; + } catch (error) { + throw new BadRequestError({ message: "Invalid IP address", error }); + } +}; + +export const isPrivateIp = (ip: string) => getIpRange(ip) !== "unicast"; diff --git a/backend/src/lib/template/validate-handlebars.ts b/backend/src/lib/template/validate-handlebars.ts new file mode 100644 index 000000000..a83c9efc2 --- /dev/null +++ b/backend/src/lib/template/validate-handlebars.ts @@ -0,0 +1,21 @@ +import handlebars from "handlebars"; + +import { BadRequestError } from "../errors"; +import { logger } from "../logger"; + +type SanitizationArg = { + allowedExpressions?: (arg: string) => boolean; +}; + +export const validateHandlebarTemplate = (templateName: string, template: string, dto: SanitizationArg) => { + const parsedAst = handlebars.parse(template); + parsedAst.body.forEach((el) => { + if (el.type === "ContentStatement") return; + if (el.type === "MustacheStatement" && "path" in el) { + const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } }; + if (path.type === "PathExpression" && dto?.allowedExpressions?.(path.original)) return; + } + logger.error(el, "Template sanitization failed"); + throw new BadRequestError({ message: `Template sanitization failed: ${templateName}` }); + }); +}; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 1d695a5ff..3dfe11d2c 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -9,6 +9,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorAuthMethod } from "../auth/auth-type"; @@ -72,6 +73,9 @@ export const projectRoleServiceFactory = ({ throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" }); } + validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const role = await projectRoleDAL.create({ ...data, projectId @@ -134,7 +138,9 @@ export const projectRoleServiceFactory = ({ if (existingRole && existingRole.id !== roleId) throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" }); } - + validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const updatedRole = await projectRoleDAL.updateById(projectRole.id, { ...data, permissions: data.permissions ? data.permissions : undefined