@just-every/mcp-screenshot-website-fast
Version:
Fast screenshot capture tool for web pages - optimized for Claude Vision API
161 lines (160 loc) • 5.42 kB
JavaScript
import { lookup } from 'dns/promises';
import { isIP } from 'net';
const SAFE_PROTOCOLS = new Set(['http:', 'https:']);
const LOCALHOST_HOSTNAMES = new Set(['localhost', 'localhost.localdomain']);
function normalizeHostname(hostname) {
return hostname.replace(/^\[|\]$/g, '').toLowerCase();
}
function parseIpv4(address) {
const parts = address.split('.');
if (parts.length !== 4)
return null;
const bytes = parts.map(part => Number(part));
if (bytes.some(byte => !Number.isInteger(byte) ||
byte < 0 ||
byte > 255 ||
!/^\d{1,3}$/.test(String(byte)))) {
return null;
}
return bytes;
}
function isBlockedIpv4(address) {
const bytes = parseIpv4(address);
if (!bytes)
return true;
const [a, b, c, d] = bytes;
return (a === 0 ||
a === 10 ||
a === 127 ||
(a === 100 && b >= 64 && b <= 127) ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 0 && c === 0) ||
(a === 192 && b === 0 && c === 2) ||
(a === 192 && b === 88 && c === 99) ||
(a === 192 && b === 168) ||
(a === 198 && (b === 18 || b === 19)) ||
(a === 198 && b === 51 && c === 100) ||
(a === 203 && b === 0 && c === 113) ||
a >= 224 ||
(a === 255 && b === 255 && c === 255 && d === 255));
}
function isLoopbackIpv4(address) {
const bytes = parseIpv4(address);
return bytes !== null && bytes[0] === 127;
}
function getMappedIpv4FromIpv6(address) {
const normalized = address.toLowerCase();
const mappedIpv4 = normalized.match(/(?:::ffff:)(\d+\.\d+\.\d+\.\d+)$/);
const mappedHexIpv4 = normalized.match(/(?:::ffff:)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
if (mappedIpv4) {
return mappedIpv4[1];
}
if (mappedHexIpv4) {
const high = parseInt(mappedHexIpv4[1], 16);
const low = parseInt(mappedHexIpv4[2], 16);
return [
(high >> 8) & 255,
high & 255,
(low >> 8) & 255,
low & 255,
].join('.');
}
return null;
}
function isBlockedIpv6(address) {
const normalized = address.toLowerCase();
const mappedAddress = getMappedIpv4FromIpv6(normalized);
if (mappedAddress) {
return isBlockedIpv4(mappedAddress);
}
return (normalized === '::' ||
normalized === '::1' ||
normalized.startsWith('fc') ||
normalized.startsWith('fd') ||
normalized.startsWith('fe8') ||
normalized.startsWith('fe9') ||
normalized.startsWith('fea') ||
normalized.startsWith('feb') ||
normalized.startsWith('ff') ||
normalized.startsWith('2001:db8:') ||
normalized === '2001:db8::');
}
function isBlockedIpAddress(address) {
const ipVersion = isIP(address);
if (ipVersion === 4) {
return isBlockedIpv4(address);
}
if (ipVersion === 6) {
return isBlockedIpv6(address);
}
return true;
}
function isLocalhostHostname(hostname) {
return LOCALHOST_HOSTNAMES.has(hostname) || hostname.endsWith('.localhost');
}
function isBlockedHostname(hostname) {
return hostname.endsWith('.local') || hostname.endsWith('.internal');
}
function isLoopbackIpv6(address) {
const normalized = address.toLowerCase();
const mappedAddress = getMappedIpv4FromIpv6(normalized);
return (normalized === '::1' ||
(mappedAddress !== null && isLoopbackIpv4(mappedAddress)));
}
function isAllowedLocalAddress(address) {
const ipVersion = isIP(address);
if (ipVersion === 4) {
return isLoopbackIpv4(address);
}
if (ipVersion === 6) {
return isLoopbackIpv6(address);
}
return false;
}
async function resolveHostname(hostname) {
const records = await lookup(hostname, {
all: true,
verbatim: false,
});
return records.map(record => record.address);
}
export async function assertSafeCaptureUrl(url) {
let parsedUrl;
try {
parsedUrl = new URL(url);
}
catch {
throw new Error(`Blocked unsafe capture URL: ${url} is not a valid URL`);
}
if (!SAFE_PROTOCOLS.has(parsedUrl.protocol)) {
throw new Error(`Blocked unsafe capture URL: ${parsedUrl.protocol || 'unknown'} URLs are not allowed`);
}
const hostname = normalizeHostname(parsedUrl.hostname);
if (!hostname) {
throw new Error(`Blocked unsafe capture URL: ${parsedUrl.hostname} is not an allowed host`);
}
if (isLocalhostHostname(hostname)) {
return;
}
if (isBlockedHostname(hostname)) {
throw new Error(`Blocked unsafe capture URL: ${parsedUrl.hostname} is not an allowed host`);
}
if (isIP(hostname)) {
if (isAllowedLocalAddress(hostname)) {
return;
}
if (isBlockedIpAddress(hostname)) {
throw new Error(`Blocked unsafe capture URL: ${hostname} is not an allowed destination`);
}
return;
}
const addresses = await resolveHostname(hostname);
if (addresses.length === 0) {
throw new Error(`Blocked unsafe capture URL: ${hostname} did not resolve to an address`);
}
const blockedAddress = addresses.find(address => isBlockedIpAddress(address));
if (blockedAddress) {
throw new Error(`Blocked unsafe capture URL: ${hostname} resolves to unsafe address ${blockedAddress}`);
}
}