feat: patched up regex issues

This commit is contained in:
=
2025-03-26 22:19:03 +05:30
parent d55ddcd577
commit c5a5ad93a8
41 changed files with 668 additions and 215 deletions
@@ -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);
+2 -2
View File
@@ -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.");
@@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability";
import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
@@ -45,7 +44,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 +60,7 @@ export const auditLogStreamServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
if (appCfg.isCloud) {
blockLocalAndPrivateIpAddresses(url);
}
await blockLocalAndPrivateIpAddresses(url);
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
if (totalStreams.length >= plan.auditLogStreamLimit) {
@@ -136,8 +132,7 @@ export const auditLogStreamServiceFactory = ({
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) 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,9 +242,7 @@ export const certificateEstServiceFactory = ({
kmsService
});
const certificates = caCertChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
@@ -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);
+3 -2
View File
@@ -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();
+1 -7
View File
@@ -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_-]$/;
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");