mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3314 from akhilmhdh/fix/ua-optimization
feat: patched up regex issues
This commit is contained in:
@@ -16,7 +16,7 @@ const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Dat
|
||||
const startDateStr = formatPartitionDate(startDate);
|
||||
const endDateStr = formatPartitionDate(endDate);
|
||||
|
||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
|
||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replaceAll("-", "")}_${endDateStr.replaceAll("-", "")}`;
|
||||
|
||||
await knex.schema.raw(
|
||||
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
|
||||
|
||||
@@ -16,7 +16,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
// for CSRs sent in PEM, we leave them as is
|
||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
||||
csrBody = csrBody.replaceAll("\n", "").replaceAll(" ", "");
|
||||
}
|
||||
|
||||
done(null, csrBody);
|
||||
|
||||
@@ -61,8 +61,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
if (ldapConfig.groupSearchBase) {
|
||||
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
|
||||
.replace(/{{\.Username}}/g, user.uid)
|
||||
.replace(/{{\.UserDN}}/g, user.dn);
|
||||
.replaceAll("{{.Username}}", user.uid)
|
||||
.replaceAll("{{.UserDN}}", user.dn);
|
||||
|
||||
if (!isValidLdapFilter(groupSearchFilter)) {
|
||||
throw new Error("Generated LDAP search filter is invalid.");
|
||||
|
||||
@@ -45,7 +45,6 @@ export const auditLogStreamServiceFactory = ({
|
||||
}: TCreateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const appCfg = getConfig();
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams) {
|
||||
throw new BadRequestError({
|
||||
@@ -62,9 +61,8 @@ export const auditLogStreamServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
if (appCfg.isCloud) {
|
||||
blockLocalAndPrivateIpAddresses(url);
|
||||
}
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||
@@ -135,9 +133,8 @@ export const auditLogStreamServiceFactory = ({
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
|
||||
if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
@@ -67,9 +68,7 @@ export const certificateEstServiceFactory = ({
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
@@ -88,10 +87,7 @@ export const certificateEstServiceFactory = ({
|
||||
const verifiedChains = await Promise.all(
|
||||
caCertChains.map((chain) => {
|
||||
const caCert = new x509.X509Certificate(chain.certificate);
|
||||
const caChain =
|
||||
chain.certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
|
||||
return isCertChainValid([cert, caCert, ...caChain]);
|
||||
})
|
||||
@@ -172,19 +168,15 @@ export const certificateEstServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!estConfig.disableBootstrapCertValidation) {
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
const caCerts = extractX509CertFromChain(estConfig.caChain)?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
@@ -250,13 +242,7 @@ export const certificateEstServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificates = caCertChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
const caCertificate = new x509.X509Certificate(caCert);
|
||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||
|
||||
@@ -95,7 +95,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
password
|
||||
});
|
||||
|
||||
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
@@ -116,7 +116,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
username
|
||||
});
|
||||
|
||||
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
|
||||
@@ -4,8 +4,9 @@ import crypto, { KeyObject } from "crypto";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
@@ -665,7 +666,7 @@ export const kmipServiceFactory = ({
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
if (isValidHostname(altName)) {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
|
||||
@@ -97,12 +97,14 @@ export const searchGroups = async (
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
const dn = entry.dn.toString();
|
||||
const regex = /cn=([^,]+)/;
|
||||
const match = dn.match(regex);
|
||||
// parse the cn from the dn
|
||||
const cn = (match && match[1]) as string;
|
||||
const cnStartIndex = dn.indexOf("cn=");
|
||||
|
||||
groups.push({ dn, cn });
|
||||
if (cnStartIndex !== -1) {
|
||||
const valueStartIndex = cnStartIndex + 3;
|
||||
const commaIndex = dn.indexOf(",", valueStartIndex);
|
||||
const cn = dn.substring(valueStartIndex, commaIndex === -1 ? undefined : commaIndex);
|
||||
groups.push({ dn, cn });
|
||||
}
|
||||
});
|
||||
res.on("error", (error) => {
|
||||
ldapClient.unbind();
|
||||
|
||||
@@ -29,15 +29,9 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
|
||||
attributeName = "name";
|
||||
}
|
||||
|
||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||
return { [attributeName]: parsedValue.replaceAll('"', "") };
|
||||
};
|
||||
|
||||
export function extractScimValueFromPath(path: string): string | null {
|
||||
const regex = /members\[value eq "([^"]+)"\]/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export const buildScimUser = ({
|
||||
orgMembershipId,
|
||||
username,
|
||||
|
||||
@@ -14,16 +14,43 @@ import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns
|
||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
||||
|
||||
const REGEX = /\${([^}]+)}/g;
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const replaceTemplateVariables = (str: string, getValue: (key: string) => unknown) => {
|
||||
// Use array to collect pieces and join at the end (more efficient for large strings)
|
||||
const parts: string[] = [];
|
||||
let pos = 0;
|
||||
|
||||
while (pos < str.length) {
|
||||
const start = str.indexOf("${", pos);
|
||||
if (start === -1) {
|
||||
parts.push(str.slice(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push(str.slice(pos, start));
|
||||
const end = str.indexOf("}", start + 2);
|
||||
|
||||
if (end === -1) {
|
||||
parts.push(str.slice(start));
|
||||
break;
|
||||
}
|
||||
|
||||
const varName = str.slice(start + 2, end);
|
||||
parts.push(String(getValue(varName)));
|
||||
pos = end + 1;
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
};
|
||||
|
||||
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
||||
if (!data) return;
|
||||
|
||||
if (typeof data === "number") return data;
|
||||
|
||||
if (typeof data === "string") {
|
||||
return data.replace(REGEX, (_a, b) => getValue(b) as string);
|
||||
return replaceTemplateVariables(data, getValue);
|
||||
}
|
||||
|
||||
if (typeof data === "object" && Array.isArray(data)) {
|
||||
|
||||
@@ -8,7 +8,18 @@ type GetFullFolderPath = {
|
||||
|
||||
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
|
||||
// Helper function to remove duplicate slashes
|
||||
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
|
||||
const removeDuplicateSlashes = (path: string) => {
|
||||
const chars = [];
|
||||
let lastWasSlash = false;
|
||||
|
||||
for (let i = 0; i < path.length; i += 1) {
|
||||
const char = path[i];
|
||||
if (char !== "/" || !lastWasSlash) chars.push(char);
|
||||
lastWasSlash = char === "/";
|
||||
}
|
||||
|
||||
return chars.join("");
|
||||
};
|
||||
|
||||
// Fetch all folders at once based on environment ID to avoid multiple queries
|
||||
const folders = await folderDAL.find({ envId });
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { isIP } from "net";
|
||||
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
|
||||
// Validates usernames or wildcard (*)
|
||||
export const isValidUserPattern = (value: string): boolean => {
|
||||
// Matches valid Linux usernames or a wildcard (*)
|
||||
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||
// Length check before regex to prevent ReDoS
|
||||
if (typeof value !== "string") return false;
|
||||
if (value.length > 32) return false; // Maximum Linux username length
|
||||
if (value === "*") return true; // Handle wildcard separately
|
||||
|
||||
// Simpler, more specific pattern for usernames
|
||||
const userRegex = /^[a-z_][a-z0-9_-]*$/i;
|
||||
return userRegex.test(value);
|
||||
};
|
||||
|
||||
// Validates hostnames, wildcard domains, or IP addresses
|
||||
export const isValidHostPattern = (value: string): boolean => {
|
||||
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||
const hostRegex =
|
||||
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||
return hostRegex.test(value);
|
||||
// Input validation
|
||||
if (typeof value !== "string") return false;
|
||||
|
||||
// Length check
|
||||
if (value.length > 255) return false;
|
||||
|
||||
// Handle the wildcard case separately
|
||||
if (value === "*") return true;
|
||||
|
||||
// Check for IP addresses using Node.js built-in functions
|
||||
if (isIP(value)) return true;
|
||||
|
||||
return isFQDN(value, {
|
||||
allow_wildcard: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { promisify } from "util";
|
||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-type
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const EXEC_TIMEOUT_MS = 10000; // 10 seconds
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSshCertSerialNumber = () => {
|
||||
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||
@@ -64,7 +66,9 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
||||
// Generate the SSH key pair
|
||||
// The "-N ''" sets an empty passphrase
|
||||
// The keys are created in the temporary directory
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
|
||||
// Read the generated keys
|
||||
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||
@@ -87,7 +91,10 @@ export const getSshPublicKey = async (privateKey: string) => {
|
||||
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||
|
||||
// Run ssh-keygen to extract the public key
|
||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||
encoding: "utf8",
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
// Ensure that files and the temporary directory are cleaned up
|
||||
@@ -143,7 +150,14 @@ export const validateSshCertificatePrincipals = (
|
||||
}
|
||||
|
||||
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
|
||||
if (
|
||||
!characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(sanitized)
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
||||
});
|
||||
@@ -266,8 +280,8 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
||||
* that it only contains alphanumeric characters with no spaces.
|
||||
*/
|
||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||
const regex = /^[A-Za-z0-9-]+$/;
|
||||
if (!regex.test(keyId)) {
|
||||
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
||||
if (!regex(keyId)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
||||
@@ -298,7 +312,7 @@ const validateSshPublicKey = async (publicKey: string) => {
|
||||
|
||||
try {
|
||||
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
|
||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile], { timeout: EXEC_TIMEOUT_MS });
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SSH public key format: could not be parsed."
|
||||
@@ -363,7 +377,7 @@ export const createSshCert = async ({
|
||||
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||
|
||||
// Execute the signing process
|
||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8", timeout: EXEC_TIMEOUT_MS });
|
||||
|
||||
// Read the signed public key from the generated cert file
|
||||
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||
|
||||
@@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
|
||||
nc += 1;
|
||||
const nonceCount = nc.toString(16).padStart(8, "0");
|
||||
const cnonce = crypto.randomBytes(24).toString("hex");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
||||
const path = opts.url;
|
||||
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
// Credit: https://github.com/miguelmota/is-base64
|
||||
export const isBase64 = (
|
||||
v: string,
|
||||
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
||||
) => {
|
||||
if (opts.allowEmpty === false && v === "") {
|
||||
return false;
|
||||
type Base64Options = {
|
||||
urlSafe?: boolean;
|
||||
padding?: boolean;
|
||||
};
|
||||
|
||||
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
|
||||
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
|
||||
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
||||
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
||||
// Default padding to true unless urlSafe is true
|
||||
const opts: Base64Options = {
|
||||
urlSafe: false,
|
||||
padding: options.urlSafe === undefined ? true : !options.urlSafe,
|
||||
...options
|
||||
};
|
||||
|
||||
if (opts.mimeRequired === true) {
|
||||
regex = mimeRegex + regex;
|
||||
} else if (opts.allowMime === true) {
|
||||
regex = `${mimeRegex}?${regex}`;
|
||||
if (str === "") return true;
|
||||
|
||||
let regex;
|
||||
if (opts.urlSafe) {
|
||||
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
|
||||
} else {
|
||||
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
|
||||
}
|
||||
|
||||
if (opts.paddingRequired === false) {
|
||||
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
||||
}
|
||||
|
||||
return new RegExp(`^${regex}$`, "gi").test(v);
|
||||
return (!opts.padding || str.length % 4 === 0) && regex.test(str);
|
||||
};
|
||||
|
||||
export const getBase64SizeInBytes = (base64String: string) => {
|
||||
|
||||
42
backend/src/lib/certificates/extract-certificate.test.ts
Normal file
42
backend/src/lib/certificates/extract-certificate.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { extractX509CertFromChain } from "./extract-certificate";
|
||||
|
||||
describe("Extract Certificate Payload", () => {
|
||||
test("Single chain", () => {
|
||||
const payload = `-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----`;
|
||||
const result = extractX509CertFromChain(payload);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(1);
|
||||
expect(result?.[0]).toEqual(payload);
|
||||
});
|
||||
|
||||
test("Multiple chain", () => {
|
||||
const payload = `-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`;
|
||||
const result = extractX509CertFromChain(payload);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(3);
|
||||
expect(result).toEqual([
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----`,
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`,
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`
|
||||
]);
|
||||
});
|
||||
});
|
||||
51
backend/src/lib/certificates/extract-certificate.ts
Normal file
51
backend/src/lib/certificates/extract-certificate.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BadRequestError } from "../errors";
|
||||
|
||||
export const extractX509CertFromChain = (certificateChain: string): string[] => {
|
||||
if (!certificateChain) {
|
||||
throw new BadRequestError({
|
||||
message: "Certificate chain is empty or undefined"
|
||||
});
|
||||
}
|
||||
|
||||
const certificates: string[] = [];
|
||||
let currentPosition = 0;
|
||||
const chainLength = certificateChain.length;
|
||||
|
||||
while (currentPosition < chainLength) {
|
||||
// Find the start of a certificate
|
||||
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
||||
const startIndex = certificateChain.indexOf(beginMarker, currentPosition);
|
||||
|
||||
if (startIndex === -1) {
|
||||
break; // No more certificates found
|
||||
}
|
||||
|
||||
// Find the end of the certificate
|
||||
const endMarker = "-----END CERTIFICATE-----";
|
||||
const endIndex = certificateChain.indexOf(endMarker, startIndex);
|
||||
|
||||
if (endIndex === -1) {
|
||||
throw new BadRequestError({
|
||||
message: "Malformed certificate chain: Found BEGIN marker without matching END marker"
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the complete certificate including markers
|
||||
const completeEndIndex = endIndex + endMarker.length;
|
||||
const certificate = certificateChain.substring(startIndex, completeEndIndex);
|
||||
|
||||
// Add the extracted certificate to our results
|
||||
certificates.push(certificate);
|
||||
|
||||
// Move position to after this certificate
|
||||
currentPosition = completeEndIndex;
|
||||
}
|
||||
|
||||
if (certificates.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message: "No valid certificates found in the chain"
|
||||
});
|
||||
}
|
||||
|
||||
return certificates;
|
||||
};
|
||||
@@ -107,12 +107,6 @@ export const isValidIp = (ip: string) => {
|
||||
return net.isIPv4(ip) || net.isIPv6(ip);
|
||||
};
|
||||
|
||||
export const isValidHostname = (name: string) => {
|
||||
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
|
||||
return hostnameRegex.test(name);
|
||||
};
|
||||
|
||||
export type TIp = {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { CharacterType, characterValidator } from "./validate-string";
|
||||
|
||||
// regex to allow only alphanumeric, dash, underscore
|
||||
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
|
||||
export const isValidFolderName = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Underscore
|
||||
]);
|
||||
|
||||
export const isValidSecretPath = (path: string) =>
|
||||
path
|
||||
|
||||
23
backend/src/lib/validator/validate-string.test.ts
Normal file
23
backend/src/lib/validator/validate-string.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CharacterType, characterValidator } from "./validate-string";
|
||||
|
||||
describe("validate-string", () => {
|
||||
test("Check alphabets", () => {
|
||||
expect(characterValidator([CharacterType.Alphabets])("hello")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.Alphabets])("hello world")).toBeFalsy();
|
||||
expect(characterValidator([CharacterType.Alphabets, CharacterType.Spaces])("hello world")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Check numbers", () => {
|
||||
expect(characterValidator([CharacterType.Numbers])("1234567890")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890-")).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Check special characters", () => {
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])("Hello-World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Plus])("Hello+World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello_World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Colon])("Hello:World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello World")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
101
backend/src/lib/validator/validate-string.ts
Normal file
101
backend/src/lib/validator/validate-string.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export enum CharacterType {
|
||||
Alphabets = "alphabets",
|
||||
Numbers = "numbers",
|
||||
AlphaNumeric = "alpha-numeric",
|
||||
Spaces = "spaces",
|
||||
SpecialCharacters = "specialCharacters",
|
||||
Punctuation = "punctuation",
|
||||
Period = "period", // .
|
||||
Underscore = "underscore", // _
|
||||
Colon = "colon", // :
|
||||
ForwardSlash = "forwardSlash", // /
|
||||
Equals = "equals", // =
|
||||
Plus = "plus", // +
|
||||
Hyphen = "hyphen", // -
|
||||
At = "at", // @
|
||||
// Additional individual characters that might be useful
|
||||
Asterisk = "asterisk", // *
|
||||
Ampersand = "ampersand", // &
|
||||
Question = "question", // ?
|
||||
Hash = "hash", // #
|
||||
Percent = "percent", // %
|
||||
Dollar = "dollar", // $
|
||||
Caret = "caret", // ^
|
||||
Backtick = "backtick", // `
|
||||
Pipe = "pipe", // |
|
||||
Backslash = "backslash", // \
|
||||
OpenParen = "openParen", // (
|
||||
CloseParen = "closeParen", // )
|
||||
OpenBracket = "openBracket", // [
|
||||
CloseBracket = "closeBracket", // ]
|
||||
OpenBrace = "openBrace", // {
|
||||
CloseBrace = "closeBrace", // }
|
||||
LessThan = "lessThan", // <
|
||||
GreaterThan = "greaterThan", // >
|
||||
SingleQuote = "singleQuote", // '
|
||||
DoubleQuote = "doubleQuote", // "
|
||||
Comma = "comma", // ,
|
||||
Semicolon = "semicolon", // ;
|
||||
Exclamation = "exclamation" // !
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string contains only specific types of characters
|
||||
*/
|
||||
export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
||||
// Create a regex pattern based on allowed character types
|
||||
const patternMap: Record<CharacterType, string> = {
|
||||
[CharacterType.Alphabets]: "a-zA-Z",
|
||||
[CharacterType.Numbers]: "0-9",
|
||||
[CharacterType.AlphaNumeric]: "a-zA-Z0-9",
|
||||
[CharacterType.Spaces]: "\\s",
|
||||
[CharacterType.SpecialCharacters]: "!@#$%^&*()_+\\-=\\[\\]{}|;:'\",.<>/?\\\\",
|
||||
[CharacterType.Punctuation]: "\\.\\,\\;\\:\\!\\?",
|
||||
[CharacterType.Colon]: "\\:",
|
||||
[CharacterType.ForwardSlash]: "\\/",
|
||||
[CharacterType.Underscore]: "_",
|
||||
[CharacterType.Hyphen]: "\\-",
|
||||
[CharacterType.Period]: "\\.",
|
||||
[CharacterType.Equals]: "=",
|
||||
[CharacterType.Plus]: "\\+",
|
||||
[CharacterType.At]: "@",
|
||||
[CharacterType.Asterisk]: "\\*",
|
||||
[CharacterType.Ampersand]: "&",
|
||||
[CharacterType.Question]: "\\?",
|
||||
[CharacterType.Hash]: "#",
|
||||
[CharacterType.Percent]: "%",
|
||||
[CharacterType.Dollar]: "\\$",
|
||||
[CharacterType.Caret]: "\\^",
|
||||
[CharacterType.Backtick]: "`",
|
||||
[CharacterType.Pipe]: "\\|",
|
||||
[CharacterType.Backslash]: "\\\\",
|
||||
[CharacterType.OpenParen]: "\\(",
|
||||
[CharacterType.CloseParen]: "\\)",
|
||||
[CharacterType.OpenBracket]: "\\[",
|
||||
[CharacterType.CloseBracket]: "\\]",
|
||||
[CharacterType.OpenBrace]: "\\{",
|
||||
[CharacterType.CloseBrace]: "\\}",
|
||||
[CharacterType.LessThan]: "<",
|
||||
[CharacterType.GreaterThan]: ">",
|
||||
[CharacterType.SingleQuote]: "'",
|
||||
[CharacterType.DoubleQuote]: '\\"',
|
||||
[CharacterType.Comma]: ",",
|
||||
[CharacterType.Semicolon]: ";",
|
||||
[CharacterType.Exclamation]: "!"
|
||||
};
|
||||
|
||||
// Combine patterns from allowed characters
|
||||
const combinedPattern = allowedCharacters.map((char) => patternMap[char]).join("");
|
||||
|
||||
// Create a regex that matches only the allowed characters
|
||||
const regex = new RegExp(`^[${combinedPattern}]+$`);
|
||||
|
||||
/**
|
||||
* Validates if the input string contains only the allowed character types
|
||||
* @param input String to validate
|
||||
* @returns Boolean indicating if the string is valid
|
||||
*/
|
||||
return function validate(input: string): boolean {
|
||||
return regex.test(input);
|
||||
};
|
||||
};
|
||||
15
backend/src/lib/validator/validate-url.test.ts
Normal file
15
backend/src/lib/validator/validate-url.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { isFQDN } from "./validate-url";
|
||||
|
||||
describe("isFQDN", () => {
|
||||
test("Non wildcard", () => {
|
||||
expect(isFQDN("www.example.com")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Wildcard", () => {
|
||||
expect(isFQDN("*.example.com", { allow_wildcard: true })).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Wildcard FQDN fails on option allow_wildcard false", () => {
|
||||
expect(isFQDN("*.example.com")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,117 @@
|
||||
import { getConfig } from "../config/env";
|
||||
import dns from "node:dns/promises";
|
||||
|
||||
import { isIPv4 } from "net";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { isPrivateIp } from "../ip/ipRange";
|
||||
|
||||
export const blockLocalAndPrivateIpAddresses = (url: string) => {
|
||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
const validUrl = new URL(url);
|
||||
const appCfg = getConfig();
|
||||
// on cloud local ips are not allowed
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
(validUrl.host === "host.docker.internal" ||
|
||||
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
validUrl.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
|
||||
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
|
||||
throw new BadRequestError({ message: "Localhost not allowed" });
|
||||
const inputHostIps: string[] = [];
|
||||
if (isIPv4(validUrl.host)) {
|
||||
inputHostIps.push(validUrl.host);
|
||||
} else {
|
||||
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
}
|
||||
const resolvedIps = await dns.resolve4(validUrl.host);
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
};
|
||||
|
||||
type FQDNOptions = {
|
||||
require_tld?: boolean;
|
||||
allow_underscores?: boolean;
|
||||
allow_trailing_dot?: boolean;
|
||||
allow_numeric_tld?: boolean;
|
||||
allow_wildcard?: boolean;
|
||||
ignore_max_length?: boolean;
|
||||
};
|
||||
|
||||
const defaultFqdnOptions: FQDNOptions = {
|
||||
require_tld: true,
|
||||
allow_underscores: false,
|
||||
allow_trailing_dot: false,
|
||||
allow_numeric_tld: false,
|
||||
allow_wildcard: false,
|
||||
ignore_max_length: false
|
||||
};
|
||||
|
||||
// credits: https://github.com/validatorjs/validator.js/blob/f5da7fb6ed59b94695e6fcb2e970c80029509919/src/lib/isFQDN.js#L13
|
||||
export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
// Apply default options
|
||||
const opts: FQDNOptions = {
|
||||
...defaultFqdnOptions,
|
||||
...options
|
||||
};
|
||||
|
||||
let testStr = str;
|
||||
/* Remove the optional trailing dot before checking validity */
|
||||
if (opts.allow_trailing_dot && str[str.length - 1] === ".") {
|
||||
testStr = testStr.substring(0, str.length - 1);
|
||||
}
|
||||
|
||||
/* Remove the optional wildcard before checking validity */
|
||||
if (opts.allow_wildcard === true && str.indexOf("*.") === 0) {
|
||||
testStr = testStr.substring(2);
|
||||
}
|
||||
|
||||
const parts = testStr.split(".");
|
||||
const tld = parts[parts.length - 1];
|
||||
|
||||
if (opts.require_tld) {
|
||||
// disallow fqdns without tld
|
||||
if (parts.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!opts.allow_numeric_tld &&
|
||||
!/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallow spaces
|
||||
if (/\s/.test(tld)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// reject numeric TLDs
|
||||
if (!opts.allow_numeric_tld && /^\d+$/.test(tld)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parts.every((part) => {
|
||||
if (part.length > 63 && !opts.ignore_max_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallow full-width chars
|
||||
if (/[\uff01-\uff5e]/.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallow parts starting or ending with hyphen
|
||||
if (/^-|-$/.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opts.allow_underscores && /_/.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
|
||||
interface SlugSchemaInputs {
|
||||
min?: number;
|
||||
max?: number;
|
||||
@@ -27,4 +29,13 @@ export const GenericResourceNameSchema = z
|
||||
.trim()
|
||||
.min(1, { message: "Name must be at least 1 character" })
|
||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Spaces
|
||||
])(val),
|
||||
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||
);
|
||||
|
||||
@@ -7,9 +7,11 @@ import { z } from "zod";
|
||||
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@@ -58,7 +60,6 @@ import {
|
||||
TSignIntermediateDTO,
|
||||
TUpdateCaDTO
|
||||
} from "./certificate-authority-types";
|
||||
import { hostnameRegex } from "./certificate-authority-validators";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<
|
||||
@@ -1017,9 +1018,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||
|
||||
// validate imported certificate and certificate chain
|
||||
const certificates = certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = extractX509CertFromChain(certificateChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
|
||||
@@ -1325,7 +1324,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
// check if the altName is a valid hostname
|
||||
if (hostnameRegex.test(altName)) {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
@@ -1702,7 +1701,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
// check if the altName is a valid hostname
|
||||
if (hostnameRegex.test(altName)) {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
@@ -9,7 +10,6 @@ const isValidDate = (dateString: string) => {
|
||||
|
||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||
|
||||
export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
export const validateAltNamesField = z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -27,7 +27,7 @@ export const validateAltNamesField = z
|
||||
if (data === "") return true;
|
||||
// Split and validate each alt name
|
||||
return data.split(", ").every((name) => {
|
||||
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
});
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ export const validateCertificateDetailsAgainstTemplate = (
|
||||
},
|
||||
template: TCertificateTemplates
|
||||
) => {
|
||||
// these are validated in router using validateTemplateRegexField
|
||||
const commonNameRegex = new RegExp(template.commonName);
|
||||
if (!commonNameRegex.test(cert.commonName)) {
|
||||
throw new BadRequestError({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
@@ -281,9 +282,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
@@ -379,9 +378,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import safe from "safe-regex";
|
||||
import z from "zod";
|
||||
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
|
||||
export const validateTemplateRegexField = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
})
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces, // (space)
|
||||
CharacterType.Asterisk, // *
|
||||
CharacterType.At, // @
|
||||
CharacterType.Hyphen, // -
|
||||
CharacterType.Period, // .
|
||||
CharacterType.Backslash // \
|
||||
])(val),
|
||||
{
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
}
|
||||
)
|
||||
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||
.refine((v) => safe(v), {
|
||||
message: "Unsafe REGEX pattern"
|
||||
|
||||
@@ -93,7 +93,8 @@ export const identityAwsAuthServiceFactory = ({
|
||||
.some((principalArn) => {
|
||||
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
|
||||
// considers exact matches + wildcard matches
|
||||
const regex = new RegExp(`^${principalArn.replace(/\*/g, ".*")}$`);
|
||||
// heavily validated in router
|
||||
const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`);
|
||||
return regex.test(extractPrincipalArn(Arn));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import safe from "safe-regex";
|
||||
import { z } from "zod";
|
||||
|
||||
const twelveDigitRegex = /^\d{12}$/;
|
||||
// akhilmhdh: change this to a normal function later. Checked no redosable at the moment
|
||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
|
||||
|
||||
export const validateAccountIds = z
|
||||
@@ -42,7 +44,8 @@ export const validatePrincipalArns = z
|
||||
// Split the string by commas to check each supposed ARN
|
||||
const arns = data.split(",");
|
||||
// Return true only if every item matches one of the allowed ARN formats
|
||||
return arns.every((arn) => arnRegex.test(arn.trim()));
|
||||
// and checks whether the provided regex is safe
|
||||
return arns.map((el) => el.trim()).every((arn) => safe(`^${arn.replaceAll("*", ".*")}$`) && arnRegex.test(arn));
|
||||
},
|
||||
{
|
||||
message:
|
||||
|
||||
@@ -584,7 +584,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
}[] = [];
|
||||
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const hyphenatedKey = key.replace(/_/g, "-");
|
||||
const hyphenatedKey = key.replaceAll("_", "-");
|
||||
if (!(hyphenatedKey in res)) {
|
||||
// case: secret has been created
|
||||
setSecrets.push({
|
||||
@@ -603,7 +603,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
const deleteSecrets: AzureKeyVaultSecret[] = [];
|
||||
|
||||
Object.keys(res).forEach((key) => {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
if (!(underscoredKey in secrets)) {
|
||||
deleteSecrets.push(res[key]);
|
||||
}
|
||||
@@ -617,7 +617,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
if (!integration.lastUsed) {
|
||||
Object.keys(res).forEach((key) => {
|
||||
// first time using integration
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
|
||||
// -> apply initial sync behavior
|
||||
switch (metadata.initialSyncBehavior) {
|
||||
@@ -3578,7 +3578,7 @@ const syncSecretsTeamCity = async ({
|
||||
.filter((parameter) => !parameter.inherited)
|
||||
.reduce(
|
||||
(obj, secret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
const secretName = secret.name.startsWith(".env") ? secret.name.slice(4) : secret.name;
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
@@ -3635,7 +3635,7 @@ const syncSecretsTeamCity = async ({
|
||||
)
|
||||
).data.property.reduce(
|
||||
(obj, secret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
const secretName = secret.name.startsWith("env.") ? secret.name.slice(4) : secret.name;
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
|
||||
@@ -43,7 +43,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||
ELSE ''
|
||||
@@ -80,7 +80,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||
ELSE ''
|
||||
@@ -119,7 +119,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc'
|
||||
ELSE ''
|
||||
|
||||
@@ -7,14 +7,16 @@ import { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { isValidSecretPath } from "@app/lib/validator";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
|
||||
|
||||
export const validateFolderName = (folderName: string) => {
|
||||
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
||||
return validNameRegex.test(folderName);
|
||||
};
|
||||
export const validateFolderName = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Underscore
|
||||
]);
|
||||
|
||||
const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => {
|
||||
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
||||
@@ -188,9 +190,9 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
|
||||
// the root folder check is used to avoid last / and also root name in folders
|
||||
depth: db.raw("parent.depth + 1"),
|
||||
path: db.raw(
|
||||
`CONCAT( CASE
|
||||
WHEN ${TableName.SecretFolder}."parentId" is NULL THEN ''
|
||||
ELSE CONCAT('/', secret_folders.name)
|
||||
`CONCAT( CASE
|
||||
WHEN ${TableName.SecretFolder}."parentId" is NULL THEN ''
|
||||
ELSE CONCAT('/', secret_folders.name)
|
||||
END, parent.path )`
|
||||
),
|
||||
child: db.raw("COALESCE(parent.child, parent.id)"),
|
||||
@@ -464,7 +466,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
db.raw("parents.depth + 1 as depth"),
|
||||
db.raw(
|
||||
`CONCAT(
|
||||
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
|
||||
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
|
||||
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
|
||||
)`
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
@@ -10,6 +11,25 @@ import {
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const tagFieldCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Equals,
|
||||
CharacterType.Plus,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.At
|
||||
]);
|
||||
|
||||
const pathCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
]);
|
||||
|
||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||
path: z
|
||||
@@ -17,35 +37,54 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
.trim()
|
||||
.min(1, "Parameter Store Path required")
|
||||
.max(2048, "Cannot exceed 2048 characters")
|
||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format')
|
||||
.refine(
|
||||
(val) =>
|
||||
val.startsWith("/") &&
|
||||
val.endsWith("/") &&
|
||||
val
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.every((el) => pathCharacterValidator(el)),
|
||||
'Invalid path - must follow "/example/path/" format'
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||
});
|
||||
|
||||
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
"Invalid KMS Key ID"
|
||||
)
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Resource tag key required")
|
||||
.max(128, "Resource tag key cannot exceed 128 characters"),
|
||||
.max(128, "Resource tag key cannot exceed 128 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -24,12 +25,23 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Plus,
|
||||
CharacterType.Equals,
|
||||
CharacterType.Period,
|
||||
CharacterType.At,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName)
|
||||
})
|
||||
])
|
||||
@@ -39,31 +51,54 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
})
|
||||
);
|
||||
|
||||
const tagFieldCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Equals,
|
||||
CharacterType.Plus,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.At
|
||||
]);
|
||||
|
||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
"Invalid KMS Key ID"
|
||||
)
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Tag key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
.max(128, "Tag key cannot exceed 128 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Tag value cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
|
||||
@@ -100,7 +100,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
const deleteSecrets: string[] = [];
|
||||
|
||||
Object.keys(secretMap).forEach((infisicalKey) => {
|
||||
const hyphenatedKey = infisicalKey.replace(/_/g, "-");
|
||||
const hyphenatedKey = infisicalKey.replaceAll("_", "-");
|
||||
if (!(hyphenatedKey in vaultSecrets)) {
|
||||
// case: secret has been created
|
||||
setSecrets.push({
|
||||
@@ -117,7 +117,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
});
|
||||
|
||||
Object.keys(vaultSecrets).forEach((key) => {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
if (!(underscoredKey in secretMap)) {
|
||||
deleteSecrets.push(key);
|
||||
}
|
||||
@@ -211,7 +211,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
);
|
||||
|
||||
for await (const [key] of Object.entries(vaultSecrets)) {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
|
||||
if (underscoredKey in secretMap) {
|
||||
if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) {
|
||||
@@ -237,7 +237,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
|
||||
Object.keys(vaultSecrets).forEach((key) => {
|
||||
if (!disabledAzureKeyVaultSecretKeys.includes(key)) {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
secretMap[underscoredKey] = {
|
||||
value: vaultSecrets[key].value
|
||||
};
|
||||
|
||||
@@ -463,7 +463,7 @@ export const recursivelyGetSecretPaths = async ({
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
return `"${val.replaceAll("\n", "\\n")}"`;
|
||||
};
|
||||
|
||||
type TSecretReferenceTraceNode = {
|
||||
|
||||
@@ -207,7 +207,7 @@ export const recursivelyGetSecretPaths = ({
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
return `"${val.replaceAll("\n", "\\n")}"`;
|
||||
};
|
||||
|
||||
type TInterpolateSecretArg = {
|
||||
@@ -218,7 +218,7 @@ type TInterpolateSecretArg = {
|
||||
};
|
||||
|
||||
const MAX_SECRET_REFERENCE_DEPTH = 5;
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
||||
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
||||
const secretCache: Record<string, Record<string, string>> = {};
|
||||
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
||||
|
||||
@@ -40,15 +40,7 @@ import {
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const validateTemplateRegexField = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message:
|
||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
});
|
||||
const validateTemplateRegexField = z.string().trim().min(1).max(100);
|
||||
|
||||
const schema = z.object({
|
||||
caId: z.string(),
|
||||
|
||||
@@ -26,21 +26,6 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
// Validates usernames or wildcard (*)
|
||||
export const isValidUserPattern = (value: string): boolean => {
|
||||
// Matches valid Linux usernames or a wildcard (*)
|
||||
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||
return userRegex.test(value);
|
||||
};
|
||||
|
||||
// Validates hostnames, wildcard domains, or IP addresses
|
||||
export const isValidHostPattern = (value: string): boolean => {
|
||||
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||
const hostRegex =
|
||||
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||
return hostRegex.test(value);
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
sshCaId: z.string(),
|
||||
@@ -69,28 +54,8 @@ const schema = z
|
||||
"Max TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
|
||||
)
|
||||
.default("30d"),
|
||||
allowedUsers: z.string().refine(
|
||||
(val) => {
|
||||
const trimmed = val.trim();
|
||||
if (trimmed === "") return true;
|
||||
const users = trimmed.split(",").map((u) => u.trim());
|
||||
return users.every(isValidUserPattern);
|
||||
},
|
||||
{
|
||||
message: "Invalid user pattern in allowedUsers"
|
||||
}
|
||||
),
|
||||
allowedHosts: z.string().refine(
|
||||
(val) => {
|
||||
const trimmed = val.trim();
|
||||
if (trimmed === "") return true;
|
||||
const users = trimmed.split(",").map((u) => u.trim());
|
||||
return users.every(isValidHostPattern);
|
||||
},
|
||||
{
|
||||
message: "Invalid host pattern in allowedHosts"
|
||||
}
|
||||
),
|
||||
allowedUsers: z.string(),
|
||||
allowedHosts: z.string(),
|
||||
allowUserCertificates: z.boolean().optional().default(false),
|
||||
allowHostCertificates: z.boolean().optional().default(false),
|
||||
allowCustomKeyIds: z.boolean().optional().default(false)
|
||||
|
||||
Reference in New Issue
Block a user