mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 08:27:36 +00:00
136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
import dns from "node:dns/promises";
|
|
|
|
import { isIPv4 } from "net";
|
|
import RE2 from "re2";
|
|
|
|
import { getConfig } from "@app/lib/config/env";
|
|
|
|
import { BadRequestError } from "../errors";
|
|
import { isPrivateIp } from "../ip/ipRange";
|
|
|
|
export const blockLocalAndPrivateIpAddresses = async (url: string, isGateway = false) => {
|
|
const appCfg = getConfig();
|
|
|
|
if (appCfg.isDevelopmentMode || isGateway) return;
|
|
|
|
const validUrl = new URL(url);
|
|
|
|
if (validUrl.username || validUrl.password) {
|
|
throw new BadRequestError({ message: "URLs with user credentials (e.g., user:pass@) are not allowed" });
|
|
}
|
|
|
|
const inputHostIps: string[] = [];
|
|
if (isIPv4(validUrl.hostname)) {
|
|
inputHostIps.push(validUrl.hostname);
|
|
} else {
|
|
if (validUrl.hostname === "localhost" || validUrl.hostname === "host.docker.internal") {
|
|
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
|
}
|
|
const resolvedIps = await dns.resolve4(validUrl.hostname);
|
|
inputHostIps.push(...resolvedIps);
|
|
}
|
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
|
if (isInternalIp && !appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)
|
|
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 &&
|
|
!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 (new RE2(/\s/).test(tld)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// reject numeric TLDs
|
|
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 (!partRegex.test(part)) {
|
|
return false;
|
|
}
|
|
|
|
// disallow full-width chars
|
|
if (fullWidthRegex.test(part)) {
|
|
return false;
|
|
}
|
|
|
|
// disallow parts starting or ending with hyphen
|
|
if (hyphenRegex.test(part)) {
|
|
return false;
|
|
}
|
|
|
|
if (!opts.allow_underscores && underscoreRegex.test(part)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
};
|