mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3299 from akhilmhdh/fix/dynamic-secret-issue
Additional validation for dynamic secret
This commit is contained in:
@@ -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 { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { isPrivateIp } from "@app/lib/ip/ipRange";
|
||||||
import { getDbConnectionHost } from "@app/lib/knex";
|
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 appCfg = getConfig();
|
||||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
// if (appCfg.NODE_ENV === "development") return; // incase you want to remove this check in dev
|
||||||
// no need for validation when it's dev
|
|
||||||
if (appCfg.NODE_ENV === "development") return;
|
|
||||||
|
|
||||||
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 (
|
// get host db ip
|
||||||
appCfg.isCloud &&
|
const exclusiveIps: string[] = [];
|
||||||
!isGateway &&
|
for await (const el of reservedHosts) {
|
||||||
// localhost
|
if (el) {
|
||||||
// internal ips
|
if (net.isIPv4(el)) {
|
||||||
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
exclusiveIps.push(el);
|
||||||
)
|
} else {
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
const resolvedIps = await dns.resolve4(el);
|
||||||
|
exclusiveIps.push(...resolvedIps);
|
||||||
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" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { customAlphabet } from "nanoid";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
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.
|
// 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));
|
CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement));
|
||||||
DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement));
|
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;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import handlebars from "handlebars";
|
|||||||
import { customAlphabet } from "nanoid";
|
import { customAlphabet } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
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";
|
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generatePassword = (size = 48) => {
|
const generatePassword = (size = 48) => {
|
||||||
@@ -20,11 +21,20 @@ const generateUsername = () => {
|
|||||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
throw new BadRequestError({ message: "Invalid db 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<typeof DynamicSecretCassandraSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ const generateUsername = () => {
|
|||||||
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
||||||
verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
|
return { ...providerInputs, host: hostIp };
|
||||||
return providerInputs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const generateUsername = () => {
|
|||||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
||||||
verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
return providerInputs;
|
return { ...providerInputs, host: hostIp };
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||||
|
|||||||
@@ -79,9 +79,8 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
|
|||||||
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
||||||
verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
|
return { ...providerInputs, host: hostIp };
|
||||||
return providerInputs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||||
@@ -51,8 +52,20 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi
|
|||||||
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
|
||||||
verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
return providerInputs;
|
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<typeof DynamicSecretRedisDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
|
||||||
@@ -27,8 +28,16 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
|||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
||||||
|
|
||||||
verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
return providerInputs;
|
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<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
|
||||||
@@ -28,8 +29,19 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
||||||
|
|
||||||
verifyHostInputValidity(providerInputs.host);
|
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||||
return providerInputs;
|
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<typeof DynamicSecretSapHanaSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
@@ -31,6 +32,18 @@ const getDaysToExpiry = (expiryDate: Date) => {
|
|||||||
export const SnowflakeProvider = (): TDynamicProviderFns => {
|
export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs);
|
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;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { withGatewayProxy } from "@app/lib/gateway";
|
import { withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
@@ -117,8 +118,21 @@ type TSqlDatabaseProviderDTO = {
|
|||||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
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<typeof DynamicSecretSqlDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||||
@@ -144,7 +158,8 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
},
|
},
|
||||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
pool: { min: 0, max: 7 }
|
||||||
});
|
});
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ActionProjectType, TableName } from "@app/db/schemas";
|
|||||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
@@ -86,6 +87,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
message: "Failed to update more privileged identity",
|
message: "Failed to update more privileged identity",
|
||||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
|
||||||
|
allowedExpressions: (val) => val.includes("identity.")
|
||||||
|
});
|
||||||
|
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@@ -173,6 +177,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), {
|
||||||
|
allowedExpressions: (val) => val.includes("identity.")
|
||||||
|
});
|
||||||
|
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ActionProjectType } from "@app/db/schemas";
|
|||||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
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" });
|
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));
|
const packedPermission = JSON.stringify(packRules(customPermission));
|
||||||
if (!dto.isTemporary) {
|
if (!dto.isTemporary) {
|
||||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
|
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
|
||||||
@@ -203,6 +208,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
|
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;
|
const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined;
|
||||||
if (isTemporary) {
|
if (isTemporary) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ActionProjectType, TableName } from "@app/db/schemas";
|
|||||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
@@ -92,6 +93,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
if (existingSlug)
|
if (existingSlug)
|
||||||
throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` });
|
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));
|
const packedPermission = JSON.stringify(packRules(customPermission));
|
||||||
if (!dto.isTemporary) {
|
if (!dto.isTemporary) {
|
||||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
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` });
|
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 isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
|
||||||
|
|
||||||
const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions));
|
const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions));
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import axios from "axios";
|
|||||||
import jmespath from "jmespath";
|
import jmespath from "jmespath";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
|
||||||
import { getDbConnectionHost } from "@app/lib/knex";
|
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns";
|
||||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||||
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
||||||
|
|
||||||
@@ -88,32 +87,14 @@ export const secretRotationDbFn = async ({
|
|||||||
variables,
|
variables,
|
||||||
options
|
options
|
||||||
}: TSecretRotationDbFn) => {
|
}: TSecretRotationDbFn) => {
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
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 [hostIp] = await verifyHostInputValidity(host);
|
||||||
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 db = knex({
|
const db = knex({
|
||||||
client,
|
client,
|
||||||
connection: {
|
connection: {
|
||||||
database,
|
database,
|
||||||
port,
|
port,
|
||||||
host,
|
host: hostIp,
|
||||||
user: username,
|
user: username,
|
||||||
password,
|
password,
|
||||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
|||||||
61
backend/src/lib/ip/ipRange.ts
Normal file
61
backend/src/lib/ip/ipRange.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { BlockList } from "node:net";
|
||||||
|
|
||||||
|
import { BadRequestError } from "../errors";
|
||||||
|
// Define BlockList instances for each range type
|
||||||
|
const ipv4RangeLists: Record<string, BlockList> = {
|
||||||
|
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";
|
||||||
21
backend/src/lib/template/validate-handlebars.ts
Normal file
21
backend/src/lib/template/validate-handlebars.ts
Normal file
@@ -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}` });
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
|
|
||||||
import { ActorAuthMethod } from "../auth/auth-type";
|
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" });
|
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({
|
const role = await projectRoleDAL.create({
|
||||||
...data,
|
...data,
|
||||||
projectId
|
projectId
|
||||||
@@ -134,7 +138,9 @@ export const projectRoleServiceFactory = ({
|
|||||||
if (existingRole && existingRole.id !== roleId)
|
if (existingRole && existingRole.id !== roleId)
|
||||||
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
|
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, {
|
const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
|
||||||
...data,
|
...data,
|
||||||
permissions: data.permissions ? data.permissions : undefined
|
permissions: data.permissions ? data.permissions : undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user