From af5b3aa1711d5dd5cb794b4ad371949144b5e77b Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Mar 2025 01:26:00 +0530 Subject: [PATCH 1/6] feat: additional validation for dynamic secret --- .../dynamic-secret/dynamic-secret-fns.ts | 61 ++++++++++++------- .../providers/aws-elasticache.ts | 9 +++ .../dynamic-secret/providers/cassandra.ts | 16 ++++- .../providers/elastic-search.ts | 2 +- .../dynamic-secret/providers/mongo-db.ts | 2 +- .../dynamic-secret/providers/rabbit-mq.ts | 2 +- .../dynamic-secret/providers/redis.ts | 15 ++++- .../dynamic-secret/providers/sap-ase.ts | 11 +++- .../dynamic-secret/providers/sap-hana.ts | 14 ++++- .../dynamic-secret/providers/snowflake.ts | 13 ++++ .../dynamic-secret/providers/sql-database.ts | 26 +++++++- ...project-additional-privilege-v2-service.ts | 8 +++ ...ty-project-additional-privilege-service.ts | 8 +++ ...oject-user-additional-privilege-service.ts | 9 +++ backend/src/lib/ip/ipRange.ts | 61 +++++++++++++++++++ .../src/lib/template/validate-handlebars.ts | 21 +++++++ .../project-role/project-role-service.ts | 8 ++- 17 files changed, 253 insertions(+), 33 deletions(-) create mode 100644 backend/src/lib/ip/ipRange.ts create mode 100644 backend/src/lib/template/validate-handlebars.ts 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..eae74d36c 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,50 @@ -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" }); }; 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..4ec798488 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,9 +21,18 @@ 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" }); + 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; }; 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..d58490f7a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -19,7 +19,7 @@ const generateUsername = () => { export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); + await verifyHostInputValidity(providerInputs.host); return providerInputs; }; 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..77f8f4f2f 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -19,7 +19,7 @@ const generateUsername = () => { export const MongoDBProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); + await verifyHostInputValidity(providerInputs.host); return providerInputs; }; 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..c76e482b3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -79,7 +79,7 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa export const RabbitMqProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); + await verifyHostInputValidity(providerInputs.host); return providerInputs; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 306b8c59c..33a5a6560 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,7 +52,19 @@ 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); + 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; }; 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..b86d3c423 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,7 +28,15 @@ export const SapAseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); + 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; }; 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..459b9298b 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,7 +29,18 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs); - verifyHostInputValidity(providerInputs.host); + 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; }; 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..86a810b98 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,7 +118,19 @@ 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)); + + 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; }; @@ -144,7 +157,16 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) } : undefined }, - acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT + acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT, + pool: { + afterCreate: (conn: unknown, done: (err: unknown, arg0: unknown) => void) => { + void verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)) + .catch((err) => { + done(err, conn); + }) + .then(() => done(null, conn)); + } + } }); 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..530136bd2 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 Create", 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..e9de176a5 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 Update", 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/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..43adcfbc8 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"; @@ -76,6 +77,9 @@ export const projectRoleServiceFactory = ({ ...data, projectId }); + validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); return { ...role, permissions: unpackPermissions(role.permissions) }; }; @@ -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 Upadte", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); const updatedRole = await projectRoleDAL.updateById(projectRole.id, { ...data, permissions: data.permissions ? data.permissions : undefined From 26a9d68823a24cd59708c646f056a8443d7add16 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Mar 2025 01:29:32 +0530 Subject: [PATCH 2/6] feat: added same for secret rotation --- .../secret-rotation-queue-fn.ts | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) 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..7de90bbb8 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,26 +87,8 @@ 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"); - + await verifyHostInputValidity(host); const db = knex({ client, connection: { From c8553fba2b04f28ee13e376392a4c0938a91e8e5 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Mar 2025 01:53:52 +0530 Subject: [PATCH 3/6] feat: updated nitpicks --- .../identity-project-additional-privilege-service.ts | 2 +- .../project-user-additional-privilege-service.ts | 2 +- backend/src/services/project-role/project-role-service.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) 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 530136bd2..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 @@ -208,7 +208,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(data.permissions || []), { + validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { allowedExpressions: (val) => val.includes("identity.") }); 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 e9de176a5..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 @@ -93,7 +93,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (existingSlug) throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); - validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(customPermission || []), { + validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), { allowedExpressions: (val) => val.includes("identity.") }); diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 43adcfbc8..3dfe11d2c 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -73,13 +73,13 @@ 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 }); - validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); return { ...role, permissions: unpackPermissions(role.permissions) }; }; @@ -138,7 +138,7 @@ 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 Upadte", JSON.stringify(data.permissions || []), { + validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), { allowedExpressions: (val) => val.includes("identity.") }); const updatedRole = await projectRoleDAL.updateById(projectRole.id, { From c179d7e5ae2cbeab4ba1b070a7ede8610c1594f5 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Mar 2025 02:22:49 +0530 Subject: [PATCH 4/6] feat: returns ip instead of host for sql --- .../services/dynamic-secret/dynamic-secret-fns.ts | 1 + .../dynamic-secret/providers/sql-database.ts | 15 ++++----------- 2 files changed, 5 insertions(+), 11 deletions(-) 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 eae74d36c..57c05b6fb 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -47,4 +47,5 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = 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/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 86a810b98..852a844ce 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -119,7 +119,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); - await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); + const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) }); @@ -131,7 +131,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) validateHandlebarTemplate("SQL revoke", providerInputs.revocationStatement, { allowedExpressions: (val) => ["username", "database"].includes(val) }); - return providerInputs; + + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { @@ -158,15 +159,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) : undefined }, acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT, - pool: { - afterCreate: (conn: unknown, done: (err: unknown, arg0: unknown) => void) => { - void verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)) - .catch((err) => { - done(err, conn); - }) - .then(() => done(null, conn)); - } - } + pool: { min: 0, max: 7 } }); return db; }; From 05005f4258f106a43c56b9c49dffa3c5841a35fa Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Mar 2025 02:24:51 +0530 Subject: [PATCH 5/6] feat: updated redis and rotation --- backend/src/ee/services/dynamic-secret/providers/redis.ts | 4 ++-- .../secret-rotation-queue/secret-rotation-queue-fn.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 33a5a6560..78efe98e3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -52,7 +52,7 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi export const RedisDatabaseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs); - await verifyHostInputValidity(providerInputs.host); + const [hostIp] = await verifyHostInputValidity(providerInputs.host); validateHandlebarTemplate("Redis creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) }); @@ -65,7 +65,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { allowedExpressions: (val) => ["username"].includes(val) }); - return providerInputs; + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => { 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 7de90bbb8..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 @@ -88,13 +88,13 @@ export const secretRotationDbFn = async ({ options }: TSecretRotationDbFn) => { const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; - await verifyHostInputValidity(host); + const [hostIp] = await verifyHostInputValidity(host); const db = knex({ client, connection: { database, port, - host, + host: hostIp, user: username, password, connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, From c936aa7157a02af8a2201fea251d9c75523a9b07 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 25 Mar 2025 02:29:50 +0530 Subject: [PATCH 6/6] feat: updated all providers --- .../src/ee/services/dynamic-secret/providers/cassandra.ts | 4 ++-- .../ee/services/dynamic-secret/providers/elastic-search.ts | 5 ++--- backend/src/ee/services/dynamic-secret/providers/mongo-db.ts | 4 ++-- .../src/ee/services/dynamic-secret/providers/rabbit-mq.ts | 5 ++--- backend/src/ee/services/dynamic-secret/providers/sap-ase.ts | 4 ++-- backend/src/ee/services/dynamic-secret/providers/sap-hana.ts | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index 4ec798488..5e4250a35 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -21,7 +21,7 @@ const generateUsername = () => { export const CassandraProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs); - await verifyHostInputValidity(providerInputs.host); + const [hostIp] = await verifyHostInputValidity(providerInputs.host); validateHandlebarTemplate("Cassandra creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password", "expiration", "keyspace"].includes(val) }); @@ -34,7 +34,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { 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 d58490f7a..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); - await 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 77f8f4f2f..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); - await 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 c76e482b3..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); - await 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/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts index b86d3c423..37e0eb36c 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -28,7 +28,7 @@ export const SapAseProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs); - await verifyHostInputValidity(providerInputs.host); + const [hostIp] = await verifyHostInputValidity(providerInputs.host); validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password"].includes(val) }); @@ -37,7 +37,7 @@ export const SapAseProvider = (): TDynamicProviderFns => { allowedExpressions: (val) => ["username"].includes(val) }); } - return providerInputs; + 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 459b9298b..7ef114c5f 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -29,7 +29,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs); - await verifyHostInputValidity(providerInputs.host); + const [hostIp] = await verifyHostInputValidity(providerInputs.host); validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, { allowedExpressions: (val) => ["username", "password", "expiration"].includes(val) }); @@ -41,7 +41,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, { allowedExpressions: (val) => ["username"].includes(val) }); - return providerInputs; + return { ...providerInputs, host: hostIp }; }; const $getClient = async (providerInputs: z.infer) => {