@langchain/core
Version:
Core LangChain.js abstractions and schemas
269 lines (267 loc) • 8.15 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
//#region src/utils/ssrf.ts
var ssrf_exports = /* @__PURE__ */ require_runtime.__exportAll({
isCloudMetadata: () => isCloudMetadata,
isLocalhost: () => isLocalhost,
isPrivateIp: () => isPrivateIp,
isSafeUrl: () => isSafeUrl,
isSameOrigin: () => isSameOrigin,
validateSafeUrl: () => validateSafeUrl
});
const PRIVATE_IP_RANGES = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"169.254.0.0/16",
"0.0.0.0/8",
"::1/128",
"fc00::/7",
"fe80::/10",
"ff00::/8"
];
const CLOUD_METADATA_IPS = [
"169.254.169.254",
"169.254.170.2",
"100.100.100.200"
];
const CLOUD_METADATA_HOSTNAMES = [
"metadata.google.internal",
"metadata",
"instance-data"
];
const LOCALHOST_NAMES = ["localhost", "localhost.localdomain"];
/**
* IPv4 regex: four octets 0-255
*/
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
/**
* Check if a string is a valid IPv4 address.
*/
function isIPv4(ip) {
return IPV4_REGEX.test(ip);
}
/**
* Check if a string is a valid IPv6 address.
* Uses expandIpv6 for validation.
*/
function isIPv6(ip) {
return expandIpv6(ip) !== null;
}
/**
* Check if a string is a valid IP address (IPv4 or IPv6).
*/
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}
/**
* Parse an IP address string to an array of integers (for IPv4) or an array of 16-bit values (for IPv6)
* Returns null if the IP is invalid.
*/
function parseIp(ip) {
if (isIPv4(ip)) return ip.split(".").map((octet) => parseInt(octet, 10));
else if (isIPv6(ip)) {
const expanded = expandIpv6(ip);
if (!expanded) return null;
const parts = expanded.split(":");
const result = [];
for (const part of parts) result.push(parseInt(part, 16));
return result;
}
return null;
}
/**
* Expand compressed IPv6 address to full form.
*/
function expandIpv6(ip) {
if (!ip || typeof ip !== "string") return null;
if (!ip.includes(":")) return null;
if (!/^[0-9a-fA-F:]+$/.test(ip)) return null;
let normalized = ip;
if (normalized.includes("::")) {
const parts = normalized.split("::");
if (parts.length > 2) return null;
const [left, right] = parts;
const leftParts = left ? left.split(":") : [];
const rightParts = right ? right.split(":") : [];
const missing = 8 - (leftParts.length + rightParts.length);
if (missing < 0) return null;
const zeros = Array(missing).fill("0");
normalized = [
...leftParts,
...zeros,
...rightParts
].filter((p) => p !== "").join(":");
}
const parts = normalized.split(":");
if (parts.length !== 8) return null;
for (const part of parts) {
if (part.length === 0 || part.length > 4) return null;
if (!/^[0-9a-fA-F]+$/.test(part)) return null;
}
return parts.map((p) => p.padStart(4, "0").toLowerCase()).join(":");
}
/**
* Parse CIDR notation (e.g., "192.168.0.0/24") into network address and prefix length.
*/
function parseCidr(cidr) {
const [addrStr, prefixStr] = cidr.split("/");
if (!addrStr || !prefixStr) return null;
const addr = parseIp(addrStr);
if (!addr) return null;
const prefixLen = parseInt(prefixStr, 10);
if (isNaN(prefixLen)) return null;
const isIpv6 = isIPv6(addrStr);
if (isIpv6 && prefixLen > 128) return null;
if (!isIpv6 && prefixLen > 32) return null;
return {
addr,
prefixLen,
isIpv6
};
}
/**
* Check if an IP address is in a given CIDR range.
*/
function isIpInCidr(ip, cidr) {
const ipParsed = parseIp(ip);
if (!ipParsed) return false;
const cidrParsed = parseCidr(cidr);
if (!cidrParsed) return false;
const isIpv6 = isIPv6(ip);
if (isIpv6 !== cidrParsed.isIpv6) return false;
const { addr: cidrAddr, prefixLen } = cidrParsed;
if (isIpv6) for (let i = 0; i < Math.ceil(prefixLen / 16); i++) {
const mask = 65535 << 16 - Math.min(16, prefixLen - i * 16) & 65535;
if ((ipParsed[i] & mask) !== (cidrAddr[i] & mask)) return false;
}
else for (let i = 0; i < Math.ceil(prefixLen / 8); i++) {
const mask = 255 << 8 - Math.min(8, prefixLen - i * 8) & 255;
if ((ipParsed[i] & mask) !== (cidrAddr[i] & mask)) return false;
}
return true;
}
/**
* Check if an IP address is private (RFC 1918, loopback, link-local, etc.)
*/
function isPrivateIp(ip) {
if (!isIP(ip)) return false;
for (const range of PRIVATE_IP_RANGES) if (isIpInCidr(ip, range)) return true;
return false;
}
/**
* Check if a hostname or IP is a known cloud metadata endpoint.
*/
function isCloudMetadata(hostname, ip) {
if (CLOUD_METADATA_IPS.includes(ip || "")) return true;
const lowerHostname = hostname.toLowerCase();
if (CLOUD_METADATA_HOSTNAMES.includes(lowerHostname)) return true;
return false;
}
/**
* Check if a hostname or IP is localhost.
*/
function isLocalhost(hostname, ip) {
if (ip) {
if (ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0") return true;
if (ip.startsWith("127.")) return true;
}
const lowerHostname = hostname.toLowerCase();
if (LOCALHOST_NAMES.includes(lowerHostname)) return true;
return false;
}
/**
* Validate that a URL is safe to connect to.
* Performs static validation checks against hostnames and direct IP addresses.
* Does not perform DNS resolution.
*
* @param url URL to validate
* @param options.allowPrivate Allow private IPs (default: false)
* @param options.allowHttp Allow http:// scheme (default: false)
* @returns The validated URL
* @throws Error if URL is not safe
*/
function validateSafeUrl(url, options) {
const allowPrivate = options?.allowPrivate ?? false;
const allowHttp = options?.allowHttp ?? false;
try {
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch {
throw new Error(`Invalid URL: ${url}`);
}
const hostname = parsedUrl.hostname;
if (!hostname) throw new Error("URL missing hostname.");
if (isCloudMetadata(hostname)) throw new Error(`URL points to cloud metadata endpoint: ${hostname}`);
if (isLocalhost(hostname)) {
if (!allowPrivate) throw new Error(`URL points to localhost: ${hostname}`);
return url;
}
const scheme = parsedUrl.protocol;
if (scheme !== "http:" && scheme !== "https:") throw new Error(`Invalid URL scheme: ${scheme}. Only http and https are allowed.`);
if (scheme === "http:" && !allowHttp) throw new Error("HTTP scheme not allowed. Use HTTPS or set allowHttp: true.");
if (isIP(hostname)) {
const ip = hostname;
if (isLocalhost(hostname, ip)) {
if (!allowPrivate) throw new Error(`URL points to localhost: ${hostname}`);
return url;
}
if (isCloudMetadata(hostname, ip)) throw new Error(`URL resolves to cloud metadata IP: ${ip} (${hostname})`);
if (isPrivateIp(ip)) {
if (!allowPrivate) throw new Error(`URL resolves to private IP: ${ip} (${hostname}). Set allowPrivate: true to allow.`);
}
return url;
}
return url;
} catch (error) {
if (error && typeof error === "object" && "message" in error) throw error;
throw new Error(`URL validation failed: ${error}`);
}
}
/**
* Check if a URL is safe to connect to (non-throwing version).
*
* @param url URL to check
* @param options.allowPrivate Allow private IPs (default: false)
* @param options.allowHttp Allow http:// scheme (default: false)
* @returns true if URL is safe, false otherwise
*/
function isSafeUrl(url, options) {
try {
validateSafeUrl(url, options);
return true;
} catch {
return false;
}
}
/**
* Check if two URLs have the same origin (scheme, host, port).
* Uses semantic URL parsing to prevent SSRF bypasses via URL variations.
*
* @param url1 First URL
* @param url2 Second URL
* @returns true if both URLs have the same origin, false otherwise
*/
function isSameOrigin(url1, url2) {
try {
return new URL(url1).origin === new URL(url2).origin;
} catch {
return false;
}
}
//#endregion
exports.isCloudMetadata = isCloudMetadata;
exports.isLocalhost = isLocalhost;
exports.isPrivateIp = isPrivateIp;
exports.isSafeUrl = isSafeUrl;
exports.isSameOrigin = isSameOrigin;
Object.defineProperty(exports, 'ssrf_exports', {
enumerable: true,
get: function () {
return ssrf_exports;
}
});
exports.validateSafeUrl = validateSafeUrl;
//# sourceMappingURL=ssrf.cjs.map