misc: moved regex use to re2

This commit is contained in:
Sheen Capadngan
2025-04-22 01:38:21 +08:00
parent eba12912f8
commit c15a1c6ed3
17 changed files with 826 additions and 44 deletions
@@ -2,6 +2,7 @@ import handlebars from "handlebars";
import ldapjs from "ldapjs";
import ldif from "ldif";
import { customAlphabet } from "nanoid";
import RE2 from "re2";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
@@ -194,7 +195,8 @@ export const LdapProvider = (): TDynamicProviderFns => {
const client = await $getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
const dnRegex = new RE2("^dn:\\s*(.+)", "m");
const dnMatch = dnRegex.exec(providerInputs.rotationLdif);
if (dnMatch) {
const username = dnMatch[1];
@@ -238,7 +240,8 @@ export const LdapProvider = (): TDynamicProviderFns => {
const client = await $getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
const dnRegex = new RE2("^dn:\\s*(.+)", "m");
const dnMatch = dnRegex.exec(providerInputs.rotationLdif);
if (dnMatch) {
const username = dnMatch[1];
@@ -1,4 +1,5 @@
import { isIP } from "net";
import RE2 from "re2";
import { isFQDN } from "@app/lib/validator/validate-url";
@@ -10,7 +11,7 @@ export const isValidUserPattern = (value: string): boolean => {
if (value === "*") return true; // Handle wildcard separately
// Simpler, more specific pattern for usernames
const userRegex = /^[a-z_][a-z0-9_-]*$/i;
const userRegex = new RE2(/^[a-z_][a-z0-9_-]*$/i);
return userRegex.test(value);
};
@@ -4,6 +4,7 @@ import { promises as fs } from "fs";
import { Knex } from "knex";
import os from "os";
import path from "path";
import RE2 from "re2";
import { promisify } from "util";
import { TSshCertificateTemplates } from "@app/db/schemas";
@@ -156,14 +157,14 @@ export const validateSshCertificatePrincipals = (
});
}
if (/\r|\n|\t|\0/.test(sanitized)) {
if (new RE2(/\r|\n|\t|\0/).test(sanitized)) {
throw new BadRequestError({
message: `Principal '${sanitized}' contains invalid whitespace or control characters.`
});
}
// disallow whitespace anywhere
if (/\s/.test(sanitized)) {
if (new RE2(/\s/).test(sanitized)) {
throw new BadRequestError({
message: `Principal '${sanitized}' cannot contain whitespace.`
});
+8 -4
View File
@@ -1,12 +1,16 @@
import RE2 from "re2";
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_-]+$/;
const base64WithPadding = new RE2(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/);
const base64WithoutPadding = new RE2(/^[A-Za-z0-9+/]+$/);
const base64UrlWithPadding = new RE2(
/^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/
);
const base64UrlWithoutPadding = new RE2(/^[A-Za-z0-9_-]+$/);
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
if (typeof str !== "string") {
+4 -1
View File
@@ -1,4 +1,5 @@
import path from "path";
import RE2 from "re2";
// given two paths irrespective of ending with / or not
// this will return true if its equal
@@ -15,4 +16,6 @@ export const prefixWithSlash = (str: string) => {
return `/${str}`;
};
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
const vowelRegex = new RE2(/^[aeiou]/i);
export const startsWithVowel = (str: string) => vowelRegex.test(str);
+2 -1
View File
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { z } from "zod";
export enum CharacterType {
@@ -92,7 +93,7 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
const combinedPattern = allowedCharacters.map((char) => patternMap[char]).join("");
// Create a regex that matches only the allowed characters
const regex = new RegExp(`^[${combinedPattern}]+$`);
const regex = new RE2(`^[${combinedPattern}]+$`);
/**
* Validates if the input string contains only the allowed character types
+13 -7
View File
@@ -1,6 +1,7 @@
import dns from "node:dns/promises";
import { isIPv4 } from "net";
import RE2 from "re2";
import { getConfig } from "@app/lib/config/env";
@@ -80,42 +81,47 @@ export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => {
if (
!opts.allow_numeric_tld &&
!/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)
!new RE2(/^([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)) {
if (new RE2(/\s/).test(tld)) {
return false;
}
}
// reject numeric TLDs
if (!opts.allow_numeric_tld && /^\d+$/.test(tld)) {
if (!opts.allow_numeric_tld && new RE2(/^\d+$/).test(tld)) {
return false;
}
const partRegex = new RE2(/^[a-z_\u00a1-\uffff0-9-]+$/i);
const fullWidthRegex = new RE2(/[\uff01-\uff5e]/);
const hyphenRegex = new RE2(/^-|-$/);
const underscoreRegex = new RE2("_");
return parts.every((part) => {
if (part.length > 63 && !opts.ignore_max_length) {
return false;
}
if (!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(part)) {
if (!partRegex.test(part)) {
return false;
}
// disallow full-width chars
if (/[\uff01-\uff5e]/.test(part)) {
if (fullWidthRegex.test(part)) {
return false;
}
// disallow parts starting or ending with hyphen
if (/^-|-$/.test(part)) {
if (hyphenRegex.test(part)) {
return false;
}
if (!opts.allow_underscores && /_/.test(part)) {
if (!opts.allow_underscores && underscoreRegex.test(part)) {
return false;
}
+6
View File
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { z, ZodTypeAny } from "zod";
// this is a patched zod string to remove empty string to undefined
@@ -11,3 +12,8 @@ export const zpStr = <T extends ZodTypeAny>(schema: T, opt: { stripNull: boolean
export const zodBuffer = z.custom<Buffer>((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
message: "Expected binary data (Buffer Or Uint8Array)"
});
export const re2Validator = (pattern: string | RegExp) => {
const re2Pattern = new RE2(pattern);
return (value: string) => re2Pattern.test(value);
};
@@ -15,6 +15,7 @@ import {
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { re2Validator } from "@app/lib/zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -316,11 +317,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: z
.string()
.trim()
.regex(
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
)
.max(64, { message: "Slug must be 64 characters or fewer" })
.refine(re2Validator(/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/), {
message:
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
})
.optional()
.describe(PROJECTS.UPDATE.slug)
}),
@@ -1,3 +1,5 @@
import RE2 from "re2";
import { TCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
@@ -12,7 +14,7 @@ export const validateCertificateDetailsAgainstTemplate = (
template: TCertificateTemplates
) => {
// these are validated in router using validateTemplateRegexField
const commonNameRegex = new RegExp(template.commonName);
const commonNameRegex = new RE2(template.commonName);
if (!commonNameRegex.test(cert.commonName)) {
throw new BadRequestError({
message: "Invalid common name based on template policy"
@@ -25,7 +27,7 @@ export const validateCertificateDetailsAgainstTemplate = (
});
}
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
const subjectAlternativeNameRegex = new RE2(template.subjectAlternativeName);
cert.altNames.forEach((altName) => {
if (!subjectAlternativeNameRegex.test(altName)) {
throw new BadRequestError({
@@ -2,6 +2,7 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import jwt from "jsonwebtoken";
import RE2 from "re2";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -125,7 +126,7 @@ export const identityAwsAuthServiceFactory = ({
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
// considers exact matches + wildcard matches
// heavily validated in router
const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`);
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
return regex.test(extractPrincipalArn(Arn));
});
@@ -1,9 +1,10 @@
import RE2 from "re2";
import safe from "safe-regex";
import { z } from "zod";
const twelveDigitRegex = /^\d{12}$/;
const twelveDigitRegex = new RE2(/^\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 = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/);
export const validateAccountIds = z
.string()
@@ -27,6 +27,7 @@ import { randomUUID } from "crypto";
import https from "https";
import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
import RE2 from "re2";
import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
@@ -4151,7 +4152,7 @@ const syncSecretsWindmill = async ({
);
// eslint-disable-next-line
const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
const pattern = new RE2("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
for await (const key of Object.keys(secrets)) {
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
if (!(key in res)) {
@@ -1,5 +1,7 @@
import path from "node:path";
import RE2 from "re2";
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@@ -13,9 +15,8 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
// akhilmhdh: JS regex with global save state in .test
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([a-zA-Z0-9-_.]+)}/;
const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`;
const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING);
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
@@ -36,7 +37,14 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3;
* // ]
*/
export const getAllSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
const references = [];
let match;
const regex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
// eslint-disable-next-line no-cond-assign
while ((match = regex.exec(maybeSecretReference)) !== null) {
references.push(match[1]);
}
const nestedReferences = references
.filter((el) => el.includes("."))
@@ -531,9 +539,17 @@ export const expandSecretReferencesFactory = ({
// eslint-disable-next-line no-continue
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
const refs = value?.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
const matchRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
const refs = [];
let match;
// eslint-disable-next-line no-cond-assign
while ((match = matchRegex.exec(value || "")) !== null) {
refs.push(match[0]);
}
if (refs.length > 0) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
@@ -592,7 +608,7 @@ export const expandSecretReferencesFactory = ({
trace
};
const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue);
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue);
if (dto.shouldStackTrace) {
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
trace?.children.push(stackTraceNode);
@@ -626,7 +642,7 @@ export const expandSecretReferencesFactory = ({
}) => {
if (!inputSecret.value) return inputSecret.value;
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value);
if (!shouldExpand) return inputSecret.value;
const { expandedValue } = await recursivelyExpandSecret(inputSecret);
+29 -7
View File
@@ -1,5 +1,6 @@
/* eslint-disable no-await-in-loop */
import path from "path";
import RE2 from "re2";
import {
ActionProjectType,
@@ -218,7 +219,9 @@ type TInterpolateSecretArg = {
};
const MAX_SECRET_REFERENCE_DEPTH = 5;
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`;
const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING);
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
@@ -273,9 +276,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
if (!value) return "";
if (depth > MAX_SECRET_REFERENCE_DEPTH) return "";
const refs = value.match(INTERPOLATION_SYNTAX_REG);
const refs = [];
let match;
const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
// eslint-disable-next-line no-cond-assign
while ((match = execRegex.exec(value)) !== null) {
refs.push(match[0]);
}
let expandedValue = value;
if (refs) {
if (refs.length > 0) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
@@ -284,7 +295,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
const [secretKey] = entities;
// eslint-disable-next-line
let referenceValue = await fetchSecret(environment, secretPath, secretKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
if (INTERPOLATION_TEST_REGEX.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment,
@@ -305,7 +316,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
// eslint-disable-next-line
let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
if (INTERPOLATION_TEST_REGEX.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment: secretReferenceEnvironment,
@@ -332,7 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
}) => {
if (!inputSecret.value) return inputSecret.value;
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value);
if (!shouldExpand) return inputSecret.value;
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
@@ -451,7 +462,18 @@ export const fnSecretBlindIndexCheckV2 = async ({
* // ]
*/
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
const matches = [];
let match;
const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
// eslint-disable-next-line no-cond-assign
while ((match = execRegex.exec(maybeSecretReference)) !== null) {
matches.push(match);
}
const references = matches.map((m) => m[1]);
return references
.filter((el) => el.includes("."))
.map((el) => {