mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 05:19:49 +00:00
feat: patched up regex issues
This commit is contained in:
@@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
|
||||
nc += 1;
|
||||
const nonceCount = nc.toString(16).padStart(8, "0");
|
||||
const cnonce = crypto.randomBytes(24).toString("hex");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replaceAll('"', "");
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replaceAll('"', "");
|
||||
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
||||
const path = opts.url;
|
||||
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
// Credit: https://github.com/miguelmota/is-base64
|
||||
export const isBase64 = (
|
||||
v: string,
|
||||
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
||||
) => {
|
||||
if (opts.allowEmpty === false && v === "") {
|
||||
return false;
|
||||
type Base64Options = {
|
||||
urlSafe?: boolean;
|
||||
padding?: boolean;
|
||||
};
|
||||
|
||||
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
|
||||
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
|
||||
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
||||
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
||||
// Default padding to true unless urlSafe is true
|
||||
const opts: Base64Options = {
|
||||
urlSafe: false,
|
||||
padding: options.urlSafe === undefined ? true : !options.urlSafe,
|
||||
...options
|
||||
};
|
||||
|
||||
if (opts.mimeRequired === true) {
|
||||
regex = mimeRegex + regex;
|
||||
} else if (opts.allowMime === true) {
|
||||
regex = `${mimeRegex}?${regex}`;
|
||||
if (str === "") return true;
|
||||
|
||||
let regex;
|
||||
if (opts.urlSafe) {
|
||||
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
|
||||
} else {
|
||||
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
|
||||
}
|
||||
|
||||
if (opts.paddingRequired === false) {
|
||||
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
||||
}
|
||||
|
||||
return new RegExp(`^${regex}$`, "gi").test(v);
|
||||
return (!opts.padding || str.length % 4 === 0) && regex.test(str);
|
||||
};
|
||||
|
||||
export const getBase64SizeInBytes = (base64String: string) => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user