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
+2 -2
View File
@@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
nc += 1;
const nonceCount = nc.toString(16).padStart(8, "0");
const cnonce = crypto.randomBytes(24).toString("hex");
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replaceAll('"', "");
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replaceAll('"', "");
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
const path = opts.url;
+27 -18
View File
@@ -1,26 +1,35 @@
// Credit: https://github.com/miguelmota/is-base64
export const isBase64 = (
v: string,
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
) => {
if (opts.allowEmpty === false && v === "") {
return false;
type Base64Options = {
urlSafe?: boolean;
padding?: boolean;
};
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
if (typeof str !== "string") {
throw new TypeError("Expected a string");
}
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
// Default padding to true unless urlSafe is true
const opts: Base64Options = {
urlSafe: false,
padding: options.urlSafe === undefined ? true : !options.urlSafe,
...options
};
if (opts.mimeRequired === true) {
regex = mimeRegex + regex;
} else if (opts.allowMime === true) {
regex = `${mimeRegex}?${regex}`;
if (str === "") return true;
let regex;
if (opts.urlSafe) {
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
} else {
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
}
if (opts.paddingRequired === false) {
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
}
return new RegExp(`^${regex}$`, "gi").test(v);
return (!opts.padding || str.length % 4 === 0) && regex.test(str);
};
export const getBase64SizeInBytes = (base64String: string) => {
@@ -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);
};
export const isValidHostname = (name: string) => {
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
return hostnameRegex.test(name);
};
export type TIp = {
ipAddress: string;
type: IPType;
@@ -1,5 +1,11 @@
import { CharacterType, characterValidator } from "./validate-string";
// regex to allow only alphanumeric, dash, underscore
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
export const isValidFolderName = characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Hyphen,
CharacterType.Underscore
]);
export const isValidSecretPath = (path: string) =>
path
@@ -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.Hyphen])("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 { isPrivateIp } from "../ip/ipRange";
export const blockLocalAndPrivateIpAddresses = (url: string) => {
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
const validUrl = new URL(url);
const appCfg = getConfig();
// on cloud local ips are not allowed
if (
appCfg.isCloud &&
(validUrl.host === "host.docker.internal" ||
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
validUrl.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
throw new BadRequestError({ message: "Localhost not allowed" });
const inputHostIps: string[] = [];
if (isIPv4(validUrl.host)) {
inputHostIps.push(validUrl.host);
} else {
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
}
const resolvedIps = await dns.resolve4(validUrl.host);
inputHostIps.push(...resolvedIps);
}
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
};
type FQDNOptions = {
require_tld?: boolean;
allow_underscores?: boolean;
allow_trailing_dot?: boolean;
allow_numeric_tld?: boolean;
allow_wildcard?: boolean;
ignore_max_length?: boolean;
};
const defaultFqdnOptions: FQDNOptions = {
require_tld: true,
allow_underscores: false,
allow_trailing_dot: false,
allow_numeric_tld: false,
allow_wildcard: false,
ignore_max_length: false
};
// credits: https://github.com/validatorjs/validator.js/blob/f5da7fb6ed59b94695e6fcb2e970c80029509919/src/lib/isFQDN.js#L13
export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => {
if (typeof str !== "string") {
throw new TypeError("Expected a string");
}
// Apply default options
const opts: FQDNOptions = {
...defaultFqdnOptions,
...options
};
let testStr = str;
/* Remove the optional trailing dot before checking validity */
if (opts.allow_trailing_dot && str[str.length - 1] === ".") {
testStr = testStr.substring(0, str.length - 1);
}
/* Remove the optional wildcard before checking validity */
if (opts.allow_wildcard === true && str.indexOf("*.") === 0) {
testStr = testStr.substring(2);
}
const parts = testStr.split(".");
const tld = parts[parts.length - 1];
if (opts.require_tld) {
// disallow fqdns without tld
if (parts.length < 2) {
return false;
}
if (
!opts.allow_numeric_tld &&
!/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)
) {
return false;
}
// disallow spaces
if (/\s/.test(tld)) {
return false;
}
}
// reject numeric TLDs
if (!opts.allow_numeric_tld && /^\d+$/.test(tld)) {
return false;
}
return parts.every((part) => {
if (part.length > 63 && !opts.ignore_max_length) {
return false;
}
if (!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(part)) {
return false;
}
// disallow full-width chars
if (/[\uff01-\uff5e]/.test(part)) {
return false;
}
// disallow parts starting or ending with hyphen
if (/^-|-$/.test(part)) {
return false;
}
if (!opts.allow_underscores && /_/.test(part)) {
return false;
}
return true;
});
};