Merge pull request #3314 from akhilmhdh/fix/ua-optimization

feat: patched up regex issues
This commit is contained in:
Maidul Islam
2025-03-26 15:25:14 -04:00
committed by GitHub
41 changed files with 669 additions and 218 deletions
@@ -16,7 +16,7 @@ const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Dat
const startDateStr = formatPartitionDate(startDate); const startDateStr = formatPartitionDate(startDate);
const endDateStr = formatPartitionDate(endDate); 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( await knex.schema.raw(
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')` `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 PEM, we leave them as is
// for CSRs sent in base64, we preprocess them to remove new lines and spaces // for CSRs sent in base64, we preprocess them to remove new lines and spaces
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) { if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, ""); csrBody = csrBody.replaceAll("\n", "").replaceAll(" ", "");
} }
done(null, csrBody); done(null, csrBody);
+2 -2
View File
@@ -61,8 +61,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
if (ldapConfig.groupSearchBase) { if (ldapConfig.groupSearchBase) {
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"; const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter) const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
.replace(/{{\.Username}}/g, user.uid) .replaceAll("{{.Username}}", user.uid)
.replace(/{{\.UserDN}}/g, user.dn); .replaceAll("{{.UserDN}}", user.dn);
if (!isValidLdapFilter(groupSearchFilter)) { if (!isValidLdapFilter(groupSearchFilter)) {
throw new Error("Generated LDAP search filter is invalid."); throw new Error("Generated LDAP search filter is invalid.");
@@ -45,7 +45,6 @@ export const auditLogStreamServiceFactory = ({
}: TCreateAuditLogStreamDTO) => { }: TCreateAuditLogStreamDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
const appCfg = getConfig();
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams) { if (!plan.auditLogStreams) {
throw new BadRequestError({ throw new BadRequestError({
@@ -62,9 +61,8 @@ export const auditLogStreamServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
if (appCfg.isCloud) { const appCfg = getConfig();
blockLocalAndPrivateIpAddresses(url); if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
}
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
if (totalStreams.length >= plan.auditLogStreamLimit) { if (totalStreams.length >= plan.auditLogStreamLimit) {
@@ -135,9 +133,8 @@ export const auditLogStreamServiceFactory = ({
const { orgId } = logStream; const { orgId } = logStream;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
const appCfg = getConfig(); const appCfg = getConfig();
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url); if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
// testing connection first // testing connection first
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
@@ -1,5 +1,6 @@
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "@app/services/certificate/certificate-fns"; import { isCertChainValid } from "@app/services/certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; 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 certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
const leafCertificate = decodeURIComponent(sslClientCert).match( const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) { if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" }); throw new UnauthorizedError({ message: "Missing client certificate" });
@@ -88,10 +87,7 @@ export const certificateEstServiceFactory = ({
const verifiedChains = await Promise.all( const verifiedChains = await Promise.all(
caCertChains.map((chain) => { caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate); const caCert = new x509.X509Certificate(chain.certificate);
const caChain = const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]); return isCertChainValid([cert, caCert, ...caChain]);
}) })
@@ -172,19 +168,15 @@ export const certificateEstServiceFactory = ({
} }
if (!estConfig.disableBootstrapCertValidation) { if (!estConfig.disableBootstrapCertValidation) {
const caCerts = estConfig.caChain const caCerts = extractX509CertFromChain(estConfig.caChain)?.map((cert) => {
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) return new x509.X509Certificate(cert);
?.map((cert) => { });
return new x509.X509Certificate(cert);
});
if (!caCerts) { if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" }); throw new BadRequestError({ message: "Failed to parse certificate chain" });
} }
const leafCertificate = decodeURIComponent(sslClientCert).match( const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) { if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" }); throw new BadRequestError({ message: "Missing client certificate" });
@@ -250,13 +242,7 @@ export const certificateEstServiceFactory = ({
kmsService kmsService
}); });
const certificates = caCertChain const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
.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 caCertificate = new x509.X509Certificate(caCert); const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]); return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
@@ -95,7 +95,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
password 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) { for await (const query of queries) {
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database. // 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 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 client = await $getClient(providerInputs);
const masterClient = await $getClient(providerInputs, true); const masterClient = await $getClient(providerInputs, true);
+3 -2
View File
@@ -4,8 +4,9 @@ import crypto, { KeyObject } from "crypto";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; 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 { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { import {
@@ -665,7 +666,7 @@ export const kmipServiceFactory = ({
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName) => {
if (isValidHostname(altName)) { if (isFQDN(altName, { allow_wildcard: true })) {
return { return {
type: "dns", type: "dns",
value: altName value: altName
@@ -97,12 +97,14 @@ export const searchGroups = async (
res.on("searchEntry", (entry) => { res.on("searchEntry", (entry) => {
const dn = entry.dn.toString(); const dn = entry.dn.toString();
const regex = /cn=([^,]+)/; const cnStartIndex = dn.indexOf("cn=");
const match = dn.match(regex);
// parse the cn from the dn
const cn = (match && match[1]) as string;
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) => { res.on("error", (error) => {
ldapClient.unbind(); ldapClient.unbind();
+1 -7
View File
@@ -29,15 +29,9 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
attributeName = "name"; 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 = ({ export const buildScimUser = ({
orgMembershipId, orgMembershipId,
username, username,
@@ -14,16 +14,43 @@ 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";
const REGEX = /\${([^}]+)}/g;
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; 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) => { export const interpolate = (data: any, getValue: (key: string) => unknown) => {
if (!data) return; if (!data) return;
if (typeof data === "number") return data; if (typeof data === "number") return data;
if (typeof data === "string") { 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)) { if (typeof data === "object" && Array.isArray(data)) {
@@ -8,7 +8,18 @@ type GetFullFolderPath = {
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => { export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
// Helper function to remove duplicate slashes // 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 // Fetch all folders at once based on environment ID to avoid multiple queries
const folders = await folderDAL.find({ envId }); 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 (*) // Validates usernames or wildcard (*)
export const isValidUserPattern = (value: string): boolean => { export const isValidUserPattern = (value: string): boolean => {
// Matches valid Linux usernames or a wildcard (*) // Length check before regex to prevent ReDoS
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/; 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); return userRegex.test(value);
}; };
// Validates hostnames, wildcard domains, or IP addresses // Validates hostnames, wildcard domains, or IP addresses
export const isValidHostPattern = (value: string): boolean => { export const isValidHostPattern = (value: string): boolean => {
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses // Input validation
const hostRegex = if (typeof value !== "string") return false;
/^(?:\*|\*\.[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); // 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 { TSshCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { import {
@@ -18,6 +19,7 @@ import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-type
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
const EXEC_TIMEOUT_MS = 10000; // 10 seconds
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
export const createSshCertSerialNumber = () => { export const createSshCertSerialNumber = () => {
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
@@ -64,7 +66,9 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
// Generate the SSH key pair // Generate the SSH key pair
// The "-N ''" sets an empty passphrase // The "-N ''" sets an empty passphrase
// The keys are created in the temporary directory // 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 // Read the generated keys
const publicKey = await fs.readFile(publicKeyFile, "utf8"); const publicKey = await fs.readFile(publicKeyFile, "utf8");
@@ -87,7 +91,10 @@ export const getSshPublicKey = async (privateKey: string) => {
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 }); await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
// Run ssh-keygen to extract the public key // 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(); return stdout.trim();
} finally { } finally {
// Ensure that files and the temporary directory are cleaned up // 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 // 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({ throw new BadRequestError({
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.` 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. * that it only contains alphanumeric characters with no spaces.
*/ */
export const validateSshCertificateKeyId = (keyId: string) => { export const validateSshCertificateKeyId = (keyId: string) => {
const regex = /^[A-Za-z0-9-]+$/; const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
if (!regex.test(keyId)) { if (!regex(keyId)) {
throw new BadRequestError({ throw new BadRequestError({
message: message:
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces." "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 { try {
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 }); 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) { } catch (error) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to validate SSH public key format: could not be parsed." 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 }); await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
// Execute the signing process // 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 // Read the signed public key from the generated cert file
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8"); const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
+2 -2
View File
@@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
nc += 1; nc += 1;
const nonceCount = nc.toString(16).padStart(8, "0"); const nonceCount = nc.toString(16).padStart(8, "0");
const cnonce = crypto.randomBytes(24).toString("hex"); const cnonce = crypto.randomBytes(24).toString("hex");
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -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].replace(/"/g, ""); 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 ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
const path = opts.url; const path = opts.url;
+27 -18
View File
@@ -1,26 +1,35 @@
// Credit: https://github.com/miguelmota/is-base64 type Base64Options = {
export const isBase64 = ( urlSafe?: boolean;
v: string, padding?: boolean;
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false } };
) => {
if (opts.allowEmpty === false && v === "") { const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
return false; 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}=)?"; // Default padding to true unless urlSafe is true
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)"; const opts: Base64Options = {
urlSafe: false,
padding: options.urlSafe === undefined ? true : !options.urlSafe,
...options
};
if (opts.mimeRequired === true) { if (str === "") return true;
regex = mimeRegex + regex;
} else if (opts.allowMime === true) { let regex;
regex = `${mimeRegex}?${regex}`; if (opts.urlSafe) {
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
} else {
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
} }
if (opts.paddingRequired === false) { return (!opts.padding || str.length % 4 === 0) && regex.test(str);
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
}
return new RegExp(`^${regex}$`, "gi").test(v);
}; };
export const getBase64SizeInBytes = (base64String: string) => { export const getBase64SizeInBytes = (base64String: string) => {
@@ -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-----`
]);
});
});
@@ -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;
};
-6
View File
@@ -107,12 +107,6 @@ export const isValidIp = (ip: string) => {
return net.isIPv4(ip) || net.isIPv6(ip); 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 = { export type TIp = {
ipAddress: string; ipAddress: string;
type: IPType; type: IPType;
@@ -1,5 +1,11 @@
import { CharacterType, characterValidator } from "./validate-string";
// regex to allow only alphanumeric, dash, underscore // 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) => export const isValidSecretPath = (path: string) =>
path path
@@ -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();
});
});
@@ -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);
};
};
@@ -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();
});
});
+113 -14
View File
@@ -1,18 +1,117 @@
import { getConfig } from "../config/env"; import dns from "node:dns/promises";
import { isIPv4 } from "net";
import { BadRequestError } from "../errors"; 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 validUrl = new URL(url);
const appCfg = getConfig(); const inputHostIps: string[] = [];
// on cloud local ips are not allowed if (isIPv4(validUrl.host)) {
if ( inputHostIps.push(validUrl.host);
appCfg.isCloud && } else {
(validUrl.host === "host.docker.internal" || if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) || throw new BadRequestError({ message: "Local IPs not allowed as URL" });
validUrl.host.match(/^192\.168\.\d+\.\d+/)) }
) const resolvedIps = await dns.resolve4(validUrl.host);
throw new BadRequestError({ message: "Local IPs not allowed as URL" }); inputHostIps.push(...resolvedIps);
}
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1") const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
throw new BadRequestError({ message: "Localhost not allowed" }); 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;
});
}; };
+12 -1
View File
@@ -1,6 +1,8 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
interface SlugSchemaInputs { interface SlugSchemaInputs {
min?: number; min?: number;
max?: number; max?: number;
@@ -27,4 +29,13 @@ export const GenericResourceNameSchema = z
.trim() .trim()
.min(1, { message: "Name must be at least 1 character" }) .min(1, { message: "Name must be at least 1 character" })
.max(64, { message: "Name must be 64 or fewer characters" }) .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 { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -58,7 +60,6 @@ import {
TSignIntermediateDTO, TSignIntermediateDTO,
TUpdateCaDTO TUpdateCaDTO
} from "./certificate-authority-types"; } from "./certificate-authority-types";
import { hostnameRegex } from "./certificate-authority-validators";
type TCertificateAuthorityServiceFactoryDep = { type TCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick< certificateAuthorityDAL: Pick<
@@ -1017,9 +1018,7 @@ export const certificateAuthorityServiceFactory = ({
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
// validate imported certificate and certificate chain // validate imported certificate and certificate chain
const certificates = certificateChain const certificates = extractX509CertFromChain(certificateChain)?.map((cert) => new x509.X509Certificate(cert));
.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" }); 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 // check if the altName is a valid hostname
if (hostnameRegex.test(altName)) { if (isFQDN(altName, { allow_wildcard: true })) {
return { return {
type: "dns", type: "dns",
value: altName value: altName
@@ -1702,7 +1701,7 @@ export const certificateAuthorityServiceFactory = ({
} }
// check if the altName is a valid hostname // check if the altName is a valid hostname
if (hostnameRegex.test(altName)) { if (isFQDN(altName, { allow_wildcard: true })) {
return { return {
type: "dns", type: "dns",
value: altName value: altName
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { isValidIp } from "@app/lib/ip"; import { isValidIp } from "@app/lib/ip";
import { isFQDN } from "@app/lib/validator/validate-url";
const isValidDate = (dateString: string) => { const isValidDate = (dateString: string) => {
const date = new Date(dateString); 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 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 export const validateAltNamesField = z
.string() .string()
.trim() .trim()
@@ -27,7 +27,7 @@ export const validateAltNamesField = z
if (data === "") return true; if (data === "") return true;
// Split and validate each alt name // Split and validate each alt name
return data.split(", ").every((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 template: TCertificateTemplates
) => { ) => {
// these are validated in router using validateTemplateRegexField
const commonNameRegex = new RegExp(template.commonName); const commonNameRegex = new RegExp(template.commonName);
if (!commonNameRegex.test(cert.commonName)) { if (!commonNameRegex.test(cert.commonName)) {
throw new BadRequestError({ throw new BadRequestError({
@@ -6,6 +6,7 @@ import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -281,9 +282,7 @@ export const certificateTemplateServiceFactory = ({
}); });
// validate CA chain // validate CA chain
const certificates = caChain const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) { if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" }); throw new BadRequestError({ message: "Failed to parse certificate chain" });
@@ -379,9 +378,7 @@ export const certificateTemplateServiceFactory = ({
}; };
if (caChain) { if (caChain) {
const certificates = caChain const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) { if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" }); throw new BadRequestError({ message: "Failed to parse certificate chain" });
@@ -1,13 +1,27 @@
import safe from "safe-regex"; import safe from "safe-regex";
import z from "zod"; import z from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
export const validateTemplateRegexField = z export const validateTemplateRegexField = z
.string() .string()
.min(1) .min(1)
.max(100) .max(100)
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, { .refine(
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed." (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 // we ensure that the inputted pattern is computationally safe by limiting star height to 1
.refine((v) => safe(v), { .refine((v) => safe(v), {
message: "Unsafe REGEX pattern" message: "Unsafe REGEX pattern"
@@ -93,7 +93,8 @@ export const identityAwsAuthServiceFactory = ({
.some((principalArn) => { .some((principalArn) => {
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$" // convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
// considers exact matches + wildcard matches // 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)); return regex.test(extractPrincipalArn(Arn));
}); });
@@ -1,6 +1,8 @@
import safe from "safe-regex";
import { z } from "zod"; import { z } from "zod";
const twelveDigitRegex = /^\d{12}$/; 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_.@+*/-]+|\*)$/; const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
export const validateAccountIds = z export const validateAccountIds = z
@@ -42,7 +44,8 @@ export const validatePrincipalArns = z
// Split the string by commas to check each supposed ARN // Split the string by commas to check each supposed ARN
const arns = data.split(","); const arns = data.split(",");
// Return true only if every item matches one of the allowed ARN formats // 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: message:
@@ -584,7 +584,7 @@ const syncSecretsAzureKeyVault = async ({
}[] = []; }[] = [];
Object.keys(secrets).forEach((key) => { Object.keys(secrets).forEach((key) => {
const hyphenatedKey = key.replace(/_/g, "-"); const hyphenatedKey = key.replaceAll("_", "-");
if (!(hyphenatedKey in res)) { if (!(hyphenatedKey in res)) {
// case: secret has been created // case: secret has been created
setSecrets.push({ setSecrets.push({
@@ -603,7 +603,7 @@ const syncSecretsAzureKeyVault = async ({
const deleteSecrets: AzureKeyVaultSecret[] = []; const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => { Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, "_"); const underscoredKey = key.replaceAll("-", "_");
if (!(underscoredKey in secrets)) { if (!(underscoredKey in secrets)) {
deleteSecrets.push(res[key]); deleteSecrets.push(res[key]);
} }
@@ -617,7 +617,7 @@ const syncSecretsAzureKeyVault = async ({
if (!integration.lastUsed) { if (!integration.lastUsed) {
Object.keys(res).forEach((key) => { Object.keys(res).forEach((key) => {
// first time using integration // first time using integration
const underscoredKey = key.replace(/-/g, "_"); const underscoredKey = key.replaceAll("-", "_");
// -> apply initial sync behavior // -> apply initial sync behavior
switch (metadata.initialSyncBehavior) { switch (metadata.initialSyncBehavior) {
@@ -3578,7 +3578,7 @@ const syncSecretsTeamCity = async ({
.filter((parameter) => !parameter.inherited) .filter((parameter) => !parameter.inherited)
.reduce( .reduce(
(obj, secret) => { (obj, secret) => {
const secretName = secret.name.replace(/^env\./, ""); const secretName = secret.name.startsWith(".env") ? secret.name.slice(4) : secret.name;
return { return {
...obj, ...obj,
[secretName]: secret.value [secretName]: secret.value
@@ -3635,7 +3635,7 @@ const syncSecretsTeamCity = async ({
) )
).data.property.reduce( ).data.property.reduce(
(obj, secret) => { (obj, secret) => {
const secretName = secret.name.replace(/^env\./, ""); const secretName = secret.name.startsWith("env.") ? secret.name.slice(4) : secret.name;
return { return {
...obj, ...obj,
[secretName]: secret.value [secretName]: secret.value
@@ -7,14 +7,16 @@ import { groupBy, removeTrailingSlash } from "@app/lib/fn";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { isValidSecretPath } from "@app/lib/validator"; import { isValidSecretPath } from "@app/lib/validator";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types"; import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
export const validateFolderName = (folderName: string) => { export const validateFolderName = characterValidator([
const validNameRegex = /^[a-zA-Z0-9-_]+$/; CharacterType.AlphaNumeric,
return validNameRegex.test(folderName); CharacterType.Hyphen,
}; CharacterType.Underscore
]);
const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => { const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => {
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2 // this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs"; 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 { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { import {
@@ -10,6 +11,25 @@ import {
} from "@app/services/secret-sync/secret-sync-schemas"; } from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; 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({ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region), region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
path: z path: z
@@ -17,35 +37,54 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
.trim() .trim()
.min(1, "Parameter Store Path required") .min(1, "Parameter Store Path required")
.max(2048, "Cannot exceed 2048 characters") .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) .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
}); });
const AwsParameterStoreSyncOptionsSchema = z.object({ const AwsParameterStoreSyncOptionsSchema = z.object({
keyId: z keyId: z
.string() .string()
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
.min(1, "Invalid KMS Key ID") .min(1, "Invalid KMS Key ID")
.max(256, "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() .optional()
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId), .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
tags: z tags: z
.object({ .object({
key: z key: z
.string() .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") .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 value: z
.string() .string()
.regex( .max(256, "Resource tag value cannot exceed 256 characters")
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u, .refine(
(val) => tagFieldCharacterValidator(val),
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-" "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() .array()
.max(50) .max(50)
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs"; 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 { 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 { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
import { SecretSync } from "@app/services/secret-sync/secret-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), .describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
secretName: z secretName: z
.string() .string()
.regex(
/^[a-zA-Z0-9/_+=.@-]+$/,
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
)
.min(1, "Secret name is required") .min(1, "Secret name is required")
.max(256, "Secret name cannot exceed 256 characters") .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) .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({ const AwsSecretsManagerSyncOptionsSchema = z.object({
keyId: z keyId: z
.string() .string()
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
.min(1, "Invalid KMS Key ID") .min(1, "Invalid KMS Key ID")
.max(256, "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() .optional()
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId), .describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
tags: z tags: z
.object({ .object({
key: z key: z
.string() .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") .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 value: z
.string() .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") .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() .array()
.max(50) .max(50)
@@ -100,7 +100,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
const deleteSecrets: string[] = []; const deleteSecrets: string[] = [];
Object.keys(secretMap).forEach((infisicalKey) => { Object.keys(secretMap).forEach((infisicalKey) => {
const hyphenatedKey = infisicalKey.replace(/_/g, "-"); const hyphenatedKey = infisicalKey.replaceAll("_", "-");
if (!(hyphenatedKey in vaultSecrets)) { if (!(hyphenatedKey in vaultSecrets)) {
// case: secret has been created // case: secret has been created
setSecrets.push({ setSecrets.push({
@@ -117,7 +117,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
}); });
Object.keys(vaultSecrets).forEach((key) => { Object.keys(vaultSecrets).forEach((key) => {
const underscoredKey = key.replace(/-/g, "_"); const underscoredKey = key.replaceAll("-", "_");
if (!(underscoredKey in secretMap)) { if (!(underscoredKey in secretMap)) {
deleteSecrets.push(key); deleteSecrets.push(key);
} }
@@ -211,7 +211,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
); );
for await (const [key] of Object.entries(vaultSecrets)) { for await (const [key] of Object.entries(vaultSecrets)) {
const underscoredKey = key.replace(/-/g, "_"); const underscoredKey = key.replaceAll("-", "_");
if (underscoredKey in secretMap) { if (underscoredKey in secretMap) {
if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) { if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) {
@@ -237,7 +237,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
Object.keys(vaultSecrets).forEach((key) => { Object.keys(vaultSecrets).forEach((key) => {
if (!disabledAzureKeyVaultSecretKeys.includes(key)) { if (!disabledAzureKeyVaultSecretKeys.includes(key)) {
const underscoredKey = key.replace(/-/g, "_"); const underscoredKey = key.replaceAll("-", "_");
secretMap[underscoredKey] = { secretMap[underscoredKey] = {
value: vaultSecrets[key].value value: vaultSecrets[key].value
}; };
@@ -463,7 +463,7 @@ export const recursivelyGetSecretPaths = async ({
const formatMultiValueEnv = (val?: string) => { const formatMultiValueEnv = (val?: string) => {
if (!val) return ""; if (!val) return "";
if (!val.match("\n")) return val; if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`; return `"${val.replaceAll("\n", "\\n")}"`;
}; };
type TSecretReferenceTraceNode = { type TSecretReferenceTraceNode = {
+2 -2
View File
@@ -207,7 +207,7 @@ export const recursivelyGetSecretPaths = ({
const formatMultiValueEnv = (val?: string) => { const formatMultiValueEnv = (val?: string) => {
if (!val) return ""; if (!val) return "";
if (!val.match("\n")) return val; if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`; return `"${val.replaceAll("\n", "\\n")}"`;
}; };
type TInterpolateSecretArg = { type TInterpolateSecretArg = {
@@ -218,7 +218,7 @@ type TInterpolateSecretArg = {
}; };
const MAX_SECRET_REFERENCE_DEPTH = 5; 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) => { export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const secretCache: Record<string, Record<string, string>> = {}; const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
@@ -40,15 +40,7 @@ import {
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const validateTemplateRegexField = z const validateTemplateRegexField = z.string().trim().min(1).max(100);
.string()
.trim()
.min(1)
.max(100)
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
message:
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
});
const schema = z.object({ const schema = z.object({
caId: z.string(), caId: z.string(),
@@ -26,21 +26,6 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; 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 const schema = z
.object({ .object({
sshCaId: z.string(), 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, ..." "Max TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
) )
.default("30d"), .default("30d"),
allowedUsers: z.string().refine( allowedUsers: z.string(),
(val) => { allowedHosts: z.string(),
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"
}
),
allowUserCertificates: z.boolean().optional().default(false), allowUserCertificates: z.boolean().optional().default(false),
allowHostCertificates: z.boolean().optional().default(false), allowHostCertificates: z.boolean().optional().default(false),
allowCustomKeyIds: z.boolean().optional().default(false) allowCustomKeyIds: z.boolean().optional().default(false)