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
JavaScript
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;
}