mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
changed url validation to use zod
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { isFQDN } from "./validate-url";
|
import { isFQDN } from "./validate-url";
|
||||||
import { isURL } from "./validate-url";
|
|
||||||
|
|
||||||
describe("isFQDN", () => {
|
describe("isFQDN", () => {
|
||||||
test("Non wildcard", () => {
|
test("Non wildcard", () => {
|
||||||
@@ -14,46 +13,3 @@ describe("isFQDN", () => {
|
|||||||
expect(isFQDN("*.example.com")).toBeFalsy();
|
expect(isFQDN("*.example.com")).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("isURL", () => {
|
|
||||||
test("Valid HTTPS URL with subdomain", () => {
|
|
||||||
expect(isURL("https://sub.domain.example.com")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Valid IPv6 host", () => {
|
|
||||||
expect(isURL("http://[2001:db8::1]")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Valid URL with query and fragment", () => {
|
|
||||||
expect(isURL("https://example.com/path?query=value#fragment")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails on missing protocol when required", () => {
|
|
||||||
expect(isURL("example.com")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails on invalid protocol", () => {
|
|
||||||
expect(isURL("abcd://example.com")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails on malformed IPv6 (missing brackets)", () => {
|
|
||||||
expect(isURL("http://2001:db8::1")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails on non-numeric port", () => {
|
|
||||||
expect(isURL("http://example.com:abc")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails on port out of range", () => {
|
|
||||||
expect(isURL("http://example.com:70000")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails on space in URL", () => {
|
|
||||||
expect(isURL("http://exa mple.com")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fails protocol-relative URL", () => {
|
|
||||||
expect(isURL("//example.com")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import dns from "node:dns/promises";
|
import dns from "node:dns/promises";
|
||||||
|
|
||||||
import { isIP, isIPv4 } from "net";
|
import { isIPv4 } from "net";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -133,114 +133,3 @@ export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
type URLValidationOptions = {
|
|
||||||
protocols?: string[];
|
|
||||||
require_tld?: boolean;
|
|
||||||
require_protocol?: boolean;
|
|
||||||
require_host?: boolean;
|
|
||||||
require_port?: boolean;
|
|
||||||
require_valid_protocol?: boolean;
|
|
||||||
allow_underscores?: boolean;
|
|
||||||
allow_trailing_dot?: boolean;
|
|
||||||
allow_protocol_relative_urls?: boolean;
|
|
||||||
validate_length?: boolean;
|
|
||||||
max_allowed_length?: number;
|
|
||||||
disallow_auth?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultUrlOptions: URLValidationOptions = {
|
|
||||||
protocols: ["http", "https", "ftp"],
|
|
||||||
require_tld: true,
|
|
||||||
require_protocol: true,
|
|
||||||
require_host: true,
|
|
||||||
require_port: false,
|
|
||||||
require_valid_protocol: true,
|
|
||||||
allow_underscores: false,
|
|
||||||
allow_trailing_dot: false,
|
|
||||||
allow_protocol_relative_urls: false,
|
|
||||||
validate_length: true,
|
|
||||||
max_allowed_length: 2084
|
|
||||||
};
|
|
||||||
|
|
||||||
// credits: https://github.com/validatorjs/validator.js/blob/f5da7fb6ed59b94695e6fcb2e970c80029509919/src/lib/isURL.js
|
|
||||||
export const isURL = (str: string, options: URLValidationOptions = {}): boolean => {
|
|
||||||
if (typeof str !== "string") {
|
|
||||||
throw new TypeError("Expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = { ...defaultUrlOptions, ...options };
|
|
||||||
|
|
||||||
if (!str || new RE2(/[\s<>]/).test(str)) return false; // Invalid chars like space, < >
|
|
||||||
|
|
||||||
if (opts.validate_length && str.length > opts.max_allowed_length!) return false; // URL too long
|
|
||||||
|
|
||||||
let protocol: string | undefined;
|
|
||||||
let host: string = "";
|
|
||||||
let hostname: string;
|
|
||||||
let port: number | undefined;
|
|
||||||
let portStr: string | undefined;
|
|
||||||
let urlWithoutAuth: string;
|
|
||||||
|
|
||||||
let split = str.split("#");
|
|
||||||
urlWithoutAuth = split.shift()!;
|
|
||||||
|
|
||||||
split = urlWithoutAuth.split("?");
|
|
||||||
urlWithoutAuth = split.shift()!;
|
|
||||||
|
|
||||||
split = urlWithoutAuth.split("://");
|
|
||||||
if (split.length > 1) {
|
|
||||||
protocol = split.shift()!.toLowerCase();
|
|
||||||
if (opts.require_valid_protocol && !opts.protocols!.includes(protocol)) return false; // Unsupported protocol
|
|
||||||
} else if (opts.require_protocol) return false; // Protocol required but missing
|
|
||||||
else if (urlWithoutAuth.startsWith("//")) {
|
|
||||||
if (!opts.allow_protocol_relative_urls) return false; // Protocol-relative not allowed
|
|
||||||
urlWithoutAuth = urlWithoutAuth.slice(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
urlWithoutAuth = split.join("://");
|
|
||||||
|
|
||||||
if (!urlWithoutAuth && !opts.require_host) return true;
|
|
||||||
|
|
||||||
split = urlWithoutAuth.split("/");
|
|
||||||
const authority = split.shift()!;
|
|
||||||
const authorityParts = authority.split("@");
|
|
||||||
|
|
||||||
if (authorityParts.length > 1) {
|
|
||||||
if (opts.disallow_auth) return false; // Auth info not allowed
|
|
||||||
const auth = authorityParts.shift()!;
|
|
||||||
if (!auth || (auth.includes(":") && auth.split(":").length > 2)) return false; // Malformed auth
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname = authorityParts.join("@");
|
|
||||||
|
|
||||||
const ipv6Match = hostname.match(/^\[([^\]]+)\](?::([0-9]+))?$/);
|
|
||||||
if (ipv6Match) {
|
|
||||||
host = ipv6Match[1];
|
|
||||||
portStr = ipv6Match[2];
|
|
||||||
} else {
|
|
||||||
const hostSplit = hostname.split(":");
|
|
||||||
host = hostSplit.shift()!;
|
|
||||||
portStr = hostSplit.length > 0 ? hostSplit.join(":") : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (portStr !== undefined) {
|
|
||||||
if (!/^[0-9]+$/.test(portStr)) return false; // Port must be numeric
|
|
||||||
port = parseInt(portStr, 10);
|
|
||||||
if (port <= 0 || port > 65535) return false; // Port out of range
|
|
||||||
} else if (opts.require_port) return false; // Port required but missing
|
|
||||||
|
|
||||||
if (!host && opts.require_host) return false; // Host required but missing
|
|
||||||
|
|
||||||
const isHostValid =
|
|
||||||
isIP(host) ||
|
|
||||||
isFQDN(host, {
|
|
||||||
require_tld: opts.require_tld,
|
|
||||||
allow_underscores: opts.allow_underscores,
|
|
||||||
allow_trailing_dot: opts.allow_trailing_dot
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isHostValid) return false; // Invalid host format
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { isValidIp } from "@app/lib/ip";
|
import { isValidIp } from "@app/lib/ip";
|
||||||
import { isFQDN, isURL } from "@app/lib/validator/validate-url";
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
|
||||||
const isValidDate = (dateString: string) => {
|
const isValidDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
@@ -15,10 +15,15 @@ export const validateAltNameField = z
|
|||||||
.trim()
|
.trim()
|
||||||
.refine(
|
.refine(
|
||||||
(name) => {
|
(name) => {
|
||||||
return isFQDN(name, { allow_wildcard: true }) || isURL(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
return (
|
||||||
|
isFQDN(name, { allow_wildcard: true }) ||
|
||||||
|
z.string().url().safeParse(name).success ||
|
||||||
|
z.string().email().safeParse(name).success ||
|
||||||
|
isValidIp(name)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "SAN must be a valid hostname, email address, or IP address"
|
message: "SAN must be a valid hostname, email address, IP address or URL"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -39,10 +44,15 @@ export const validateAltNamesField = z
|
|||||||
if (data === "") return true;
|
if (data === "") return true;
|
||||||
// Split and validate each alt name
|
// Split and validate each alt name
|
||||||
return data.split(", ").every((name) => {
|
return data.split(", ").every((name) => {
|
||||||
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
return (
|
||||||
|
isFQDN(name, { allow_wildcard: true }) ||
|
||||||
|
z.string().url().safeParse(name).success ||
|
||||||
|
z.string().email().safeParse(name).success ||
|
||||||
|
isValidIp(name)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Each alt name must be a valid hostname or email address"
|
message: "Each alt name must be a valid hostname, email address, IP address or URL"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
+9
-10
@@ -9,8 +9,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { isFQDN, isURL } from "@app/lib/validator/validate-url";
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
import { isIP } from "net";
|
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
@@ -161,11 +160,11 @@ export const InternalCertificateAuthorityFns = ({
|
|||||||
return { type: "email", value: altName };
|
return { type: "email", value: altName };
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isURL(altName)) {
|
if (z.string().url().safeParse(altName).success) {
|
||||||
return { type: "url", value: altName };
|
return { type: "url", value: altName };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isIP(altName)) {
|
if (z.string().ip().safeParse(altName).success) {
|
||||||
return { type: "ip", value: altName };
|
return { type: "ip", value: altName };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,18 +434,18 @@ export const InternalCertificateAuthorityFns = ({
|
|||||||
return { type: "email", value: altName };
|
return { type: "email", value: altName };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
if (z.string().url().safeParse(altName).success) {
|
||||||
return { type: "dns", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isURL(altName)) {
|
|
||||||
return { type: "url", value: altName };
|
return { type: "url", value: altName };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isIP(altName)) {
|
if (z.string().ip().safeParse(altName).success) {
|
||||||
return { type: "ip", value: altName };
|
return { type: "ip", value: altName };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||||
|
return { type: "dns", value: altName };
|
||||||
|
}
|
||||||
|
|
||||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user