File size: 3,395 Bytes
e67ab0e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
/**
* URL validation and sanitization utilities for MCP integration
*/
import { browser } from "$app/environment";
import { dev } from "$app/environment";
/**
* Sanitize and validate a URL for MCP server connections
* @param urlString - The URL string to validate
* @returns Sanitized URL string or null if invalid
*/
export function validateMcpServerUrl(urlString: string): string | null {
if (!urlString || typeof urlString !== "string") {
return null;
}
try {
const url = new URL(urlString.trim());
// Allow http/https only
if (!["http:", "https:"].includes(url.protocol)) {
return null;
}
// Warn about non-HTTPS in production
if (!dev && url.protocol === "http:" && browser) {
console.warn(
"Warning: Connecting to non-HTTPS MCP server in production. This may expose sensitive data."
);
}
// Block certain localhost/private IPs in production
if (!dev && isPrivateOrLocalhost(url.hostname)) {
console.warn("Warning: Localhost/private IP addresses are not recommended in production.");
}
return url.toString();
} catch (error) {
// Invalid URL
return null;
}
}
/**
* Check if hostname is localhost or a private IP
*/
function isPrivateOrLocalhost(hostname: string): boolean {
// Localhost checks
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1" ||
hostname.endsWith(".localhost")
) {
return true;
}
// Private IP ranges (IPv4)
const ipv4Regex = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|0\.0\.0\.0|169\.254\.)/;
if (ipv4Regex.test(hostname)) {
return true;
}
return false;
}
/**
* Sanitize URL by removing sensitive parts
* Used for logging and display purposes
*/
export function sanitizeUrlForDisplay(urlString: string): string {
try {
const url = new URL(urlString);
// Remove username/password if present
url.username = "";
url.password = "";
return url.toString();
} catch {
return urlString;
}
}
/**
* Check if URL is safe to connect to
* Returns an error message if unsafe, null if safe
*/
export function checkUrlSafety(urlString: string): string | null {
const validated = validateMcpServerUrl(urlString);
if (!validated) {
return "Invalid URL. Please use http:// or https:// URLs only.";
}
try {
const url = new URL(validated);
// Additional safety checks
if (!dev && url.protocol === "http:") {
return "Non-HTTPS URLs are not recommended in production. Please use https:// for security.";
}
return null; // Safe
} catch {
return "Invalid URL format.";
}
}
/**
* Check if a header key is likely to contain sensitive data
*/
export function isSensitiveHeader(key: string): boolean {
const sensitiveKeys = [
"authorization",
"api-key",
"api_key",
"apikey",
"token",
"secret",
"password",
"bearer",
"x-api-key",
"x-auth-token",
];
const lowerKey = key.toLowerCase();
return sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive));
}
/**
* Validate header key-value pair
* Returns error message if invalid, null if valid
*/
export function validateHeader(key: string, value: string): string | null {
if (!key || !key.trim()) {
return "Header name is required";
}
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
return "Header name can only contain letters, numbers, hyphens, and underscores";
}
if (!value) {
return "Header value is required";
}
return null;
}
|