UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

118 lines (117 loc) • 4.58 kB
import { matchPattern } from "@astrojs/internal-helpers/remote"; /** * Parses a potentially comma-separated multi-value header (as produced by * proxy chains) and returns the first value, trimmed of whitespace. * Returns `undefined` when the header is absent or empty. */ export function getFirstForwardedValue(multiValueHeader) { return multiValueHeader?.toString().split(",").map((e) => e.trim())[0]; } /** * Sanitize a hostname by rejecting any with path separators. * Prevents path injection attacks. Invalid hostnames return undefined. */ function sanitizeHost(hostname) { if (!hostname) return undefined; // Reject any hostname containing path separators - they're invalid if (/[/\\]/.test(hostname)) return undefined; return hostname; } /** * Parse a host string into hostname and port components. Returns `undefined` * for a host that carries more than a single `hostname:port` pair (e.g. * `example.com:8080:8080`), which is not a valid host and would otherwise be * accepted by inspecting only the first two segments. */ function parseHost(host) { const parts = host.split(":"); if (parts.length > 2) return undefined; return { hostname: parts[0], port: parts[1] }; } /** * Check if a host matches any of the allowed domain patterns. * Assumes hostname and port are already sanitized/parsed. */ function matchesAllowedDomains(hostname, protocol, port, allowedDomains) { const hostWithPort = port ? `${hostname}:${port}` : hostname; const urlString = `${protocol}://${hostWithPort}`; if (!URL.canParse(urlString)) { return false; } const testUrl = new URL(urlString); return allowedDomains.some((pattern) => matchPattern(testUrl, pattern)); } /** * Validate a host against allowedDomains. * Returns the host only if it matches an allowed pattern; otherwise, undefined. * This prevents SSRF attacks by ensuring the Host header is trusted. */ export function validateHost(host, protocol, allowedDomains) { if (!host || host.length === 0) return undefined; if (!allowedDomains || allowedDomains.length === 0) return undefined; const sanitized = sanitizeHost(host); if (!sanitized) return undefined; const parsed = parseHost(sanitized); if (!parsed) return undefined; const { hostname, port } = parsed; if (matchesAllowedDomains(hostname, protocol, port, allowedDomains)) { return sanitized; } return undefined; } /** * Validate forwarded headers (proto, host, port) against allowedDomains. * Returns validated values or undefined for rejected headers. * Uses strict defaults: http/https only for proto, rejects port if not in allowedDomains. */ export function validateForwardedHeaders(forwardedProtocol, forwardedHost, forwardedPort, allowedDomains) { const result = {}; // Validate protocol if (forwardedProtocol) { if (allowedDomains && allowedDomains.length > 0) { const hasProtocolPatterns = allowedDomains.some((pattern) => pattern.protocol !== undefined); if (hasProtocolPatterns) { // Only validate the protocol here; host+proto combination is checked in the host block below try { const testUrl = new URL(`${forwardedProtocol}://example.com`); const isAllowed = allowedDomains.some((pattern) => matchPattern(testUrl, { protocol: pattern.protocol })); if (isAllowed) { result.protocol = forwardedProtocol; } } catch {} } else if (/^https?$/.test(forwardedProtocol)) { // allowedDomains exist but no protocol patterns, allow http/https result.protocol = forwardedProtocol; } } } // Validate port first if (forwardedPort && allowedDomains && allowedDomains.length > 0) { const hasPortPatterns = allowedDomains.some((pattern) => pattern.port !== undefined); if (hasPortPatterns) { // Validate against allowedDomains patterns const isAllowed = allowedDomains.some((pattern) => pattern.port === forwardedPort); if (isAllowed) { result.port = forwardedPort; } } } // Validate host (extract port from hostname for validation) // Reject empty strings and sanitize to prevent path injection if (forwardedHost && forwardedHost.length > 0 && allowedDomains && allowedDomains.length > 0) { const protoForValidation = result.protocol || "https"; const sanitized = sanitizeHost(forwardedHost); const parsed = sanitized ? parseHost(sanitized) : undefined; if (sanitized && parsed) { const { hostname, port: portFromHost } = parsed; const portForValidation = result.port || portFromHost; if (matchesAllowedDomains(hostname, protoForValidation, portForValidation, allowedDomains)) { result.host = sanitized; } } } return result; }