Skip to main content
Glama
security.test.ts19.7 kB
/** * Unit tests for security utilities * Tests Origin and Host header validation for DNS rebinding attack prevention */ import { describe, it, expect } from "vitest"; import { validateOrigin, validateHost, getCorsOptions, getMatchingOrigin, getCorsOptionsForRequest, } from "../../../src/lib/security"; import { CORS_CONFIG } from "../../../src/config"; describe("validateOrigin", () => { describe("valid origins", () => { it("should accept production HTTPS origins", () => { expect(validateOrigin("https://mcp.globalping.io")).toBe(true); expect(validateOrigin("https://mcp.globalping.dev")).toBe(true); }); it("should accept localhost HTTP origins without port", () => { expect(validateOrigin("http://localhost")).toBe(true); expect(validateOrigin("http://127.0.0.1")).toBe(true); expect(validateOrigin("http://[::1]")).toBe(true); }); it("should accept localhost HTTPS origins without port", () => { expect(validateOrigin("https://localhost")).toBe(true); expect(validateOrigin("https://127.0.0.1")).toBe(true); expect(validateOrigin("https://[::1]")).toBe(true); }); it("should accept localhost with port numbers", () => { expect(validateOrigin("http://localhost:3000")).toBe(true); expect(validateOrigin("http://localhost:8080")).toBe(true); expect(validateOrigin("http://127.0.0.1:3000")).toBe(true); expect(validateOrigin("http://127.0.0.1:8080")).toBe(true); expect(validateOrigin("http://[::1]:8080")).toBe(true); }); it("should accept localhost HTTPS with port numbers", () => { expect(validateOrigin("https://localhost:3000")).toBe(true); expect(validateOrigin("https://localhost:8443")).toBe(true); expect(validateOrigin("https://127.0.0.1:3000")).toBe(true); expect(validateOrigin("https://127.0.0.1:8443")).toBe(true); expect(validateOrigin("https://[::1]:8443")).toBe(true); expect(validateOrigin("https://[::1]:3000")).toBe(true); }); it("should accept MCP client protocol schemes", () => { expect(validateOrigin("vscode://")).toBe(true); expect(validateOrigin("claude://")).toBe(true); }); it("should handle exact matches from whitelist", () => { for (const origin of CORS_CONFIG.ALLOWED_ORIGINS) { expect(validateOrigin(origin)).toBe(true); } }); }); describe("invalid origins", () => { it("should reject null origin", () => { expect(validateOrigin(null)).toBe(false); }); it("should reject empty string origin", () => { expect(validateOrigin("")).toBe(false); }); it("should reject malicious external origins", () => { expect(validateOrigin("https://evil.com")).toBe(false); expect(validateOrigin("http://malicious-site.com")).toBe(false); expect(validateOrigin("https://attacker.example.com")).toBe(false); }); it("should reject origin with wrong protocol", () => { expect(validateOrigin("ftp://localhost")).toBe(false); expect(validateOrigin("ws://localhost")).toBe(false); }); it("should reject subdomains that are not whitelisted", () => { expect(validateOrigin("https://subdomain.mcp.globalping.io")).toBe(false); expect(validateOrigin("https://evil.globalping.io")).toBe(false); }); it("should reject similar but different domains", () => { expect(validateOrigin("https://mcp.globalping.io.evil.com")).toBe(false); expect(validateOrigin("https://mcpglobalping.io")).toBe(false); expect(validateOrigin("https://mcp-globalping.io")).toBe(false); }); it("should reject invalid URL formats", () => { expect(validateOrigin("not-a-url")).toBe(false); expect(validateOrigin("javascript:alert(1)")).toBe(false); expect(validateOrigin("data:text/html")).toBe(false); }); it("should reject IP addresses that are not 127.0.0.1", () => { expect(validateOrigin("http://192.168.1.1")).toBe(false); expect(validateOrigin("http://10.0.0.1")).toBe(false); expect(validateOrigin("http://172.16.0.1")).toBe(false); }); it("should reject localhost-like domains", () => { expect(validateOrigin("http://localhost.evil.com")).toBe(false); expect(validateOrigin("http://127.0.0.1.evil.com")).toBe(false); }); }); describe("edge cases", () => { it("should handle origins with trailing slashes", () => { // URLs automatically normalize trailing slashes in the origin const urlWithSlash = new URL("https://mcp.globalping.io/"); expect(validateOrigin(urlWithSlash.origin)).toBe(true); }); it("should handle origins with paths (origin doesn't include path)", () => { const urlWithPath = new URL("https://mcp.globalping.io/some/path"); expect(validateOrigin(urlWithPath.origin)).toBe(true); }); it("should handle origins with query strings (origin doesn't include query)", () => { const urlWithQuery = new URL("https://mcp.globalping.io?param=value"); expect(validateOrigin(urlWithQuery.origin)).toBe(true); }); it("should be case-sensitive for protocols", () => { // URLs normalize protocol to lowercase const url = new URL("HTTPS://mcp.globalping.io"); expect(validateOrigin(url.origin)).toBe(true); }); }); describe("DNS rebinding attack prevention", () => { it("should prevent DNS rebinding with spoofed origins", () => { // Attacker tries to make their domain resolve to 127.0.0.1 expect(validateOrigin("http://attacker.com")).toBe(false); expect(validateOrigin("http://127.0.0.1.attacker.com")).toBe(false); }); it("should prevent homograph attacks", () => { // Using unicode characters that look similar expect(validateOrigin("https://mсp.globalping.io")).toBe(false); // Cyrillic 'с' }); it("should prevent protocol downgrade attacks", () => { // If production is HTTPS, HTTP should be rejected expect(validateOrigin("http://mcp.globalping.io")).toBe(false); expect(validateOrigin("http://mcp.globalping.dev")).toBe(false); }); it("should require exact port match for production hosts", () => { // Production hosts should NOT allow port variations // Only localhost/127.0.0.1 should allow different ports expect(validateOrigin("https://mcp.globalping.io:8443")).toBe(false); expect(validateOrigin("https://mcp.globalping.io:443")).toBe(false); expect(validateOrigin("https://mcp.globalping.dev:8080")).toBe(false); // Standard HTTPS port (443) is implicit, but explicit port should be rejected }); }); }); describe("getCorsOptions", () => { it("should return valid CORS configuration", () => { const corsOptions = getCorsOptions(); expect(corsOptions).toHaveProperty("origin"); expect(corsOptions).toHaveProperty("methods"); expect(corsOptions).toHaveProperty("headers"); expect(corsOptions).toHaveProperty("exposeHeaders"); expect(corsOptions).toHaveProperty("maxAge"); }); it("should include all allowed origins as an array", () => { const corsOptions = getCorsOptions(); expect(Array.isArray(corsOptions.origin)).toBe(true); expect(corsOptions.origin.length).toBe(CORS_CONFIG.ALLOWED_ORIGINS.length); for (const origin of CORS_CONFIG.ALLOWED_ORIGINS) { expect(corsOptions.origin).toContain(origin); } }); it("should include proper HTTP methods", () => { const corsOptions = getCorsOptions(); expect(corsOptions.methods).toBe(CORS_CONFIG.METHODS); expect(corsOptions.methods).toContain("GET"); expect(corsOptions.methods).toContain("POST"); expect(corsOptions.methods).toContain("DELETE"); expect(corsOptions.methods).toContain("OPTIONS"); }); it("should include proper headers", () => { const corsOptions = getCorsOptions(); expect(corsOptions.headers).toBe(CORS_CONFIG.HEADERS); expect(corsOptions.headers).toContain("Content-Type"); expect(corsOptions.headers).toContain("Authorization"); }); it("should have a reasonable maxAge value", () => { const corsOptions = getCorsOptions(); expect(corsOptions.maxAge).toBe(CORS_CONFIG.MAX_AGE); expect(corsOptions.maxAge).toBe(86400); // 24 hours }); it("should expose Mcp-Session-Id header for browser clients", () => { const corsOptions = getCorsOptions(); expect(corsOptions.exposeHeaders).toBe(CORS_CONFIG.EXPOSE_HEADERS); expect(corsOptions.exposeHeaders).toContain("Mcp-Session-Id"); }); it("should allow Mcp-Session-Id in request headers", () => { const corsOptions = getCorsOptions(); expect(corsOptions.headers).toContain("Mcp-Session-Id"); }); }); describe("getMatchingOrigin", () => { it("should return exact match for production origins", () => { expect(getMatchingOrigin("https://mcp.globalping.io")).toBe("https://mcp.globalping.io"); expect(getMatchingOrigin("https://mcp.globalping.dev")).toBe("https://mcp.globalping.dev"); }); it("should return exact match for localhost with ports", () => { expect(getMatchingOrigin("http://localhost:3000")).toBe("http://localhost"); expect(getMatchingOrigin("http://127.0.0.1:8080")).toBe("http://127.0.0.1"); }); it("should return base origin for localhost without ports", () => { expect(getMatchingOrigin("http://localhost")).toBe("http://localhost"); expect(getMatchingOrigin("https://127.0.0.1")).toBe("https://127.0.0.1"); }); it("should return exact match for IPv6 localhost with ports", () => { expect(getMatchingOrigin("http://[::1]:3000")).toBe("http://[::1]"); expect(getMatchingOrigin("http://[::1]:8080")).toBe("http://[::1]"); expect(getMatchingOrigin("https://[::1]:8443")).toBe("https://[::1]"); expect(getMatchingOrigin("https://[::1]:3000")).toBe("https://[::1]"); }); it("should return base origin for IPv6 localhost without ports", () => { expect(getMatchingOrigin("http://[::1]")).toBe("http://[::1]"); expect(getMatchingOrigin("https://[::1]")).toBe("https://[::1]"); }); it("should return null for invalid origins", () => { expect(getMatchingOrigin("https://evil.com")).toBeNull(); expect(getMatchingOrigin("http://attacker.example.com")).toBeNull(); expect(getMatchingOrigin(null)).toBeNull(); expect(getMatchingOrigin("")).toBeNull(); }); it("should reject production origins with non-standard ports", () => { expect(getMatchingOrigin("https://mcp.globalping.io:8443")).toBeNull(); expect(getMatchingOrigin("https://mcp.globalping.dev:3000")).toBeNull(); }); it("should handle MCP client protocols", () => { expect(getMatchingOrigin("vscode://")).toBe("vscode://"); expect(getMatchingOrigin("claude://")).toBe("claude://"); }); }); describe("getCorsOptionsForRequest", () => { it("should return single origin for valid browser requests", () => { // Mock Request with Origin header (Origin is a forbidden header in Fetch API) const mockRequest = { headers: { get: (name: string) => name.toLowerCase() === "origin" ? "http://localhost:3000" : null, }, } as unknown as Request; const corsOptions = getCorsOptionsForRequest(mockRequest); expect(corsOptions.origin).toBe("http://localhost"); expect(typeof corsOptions.origin).toBe("string"); }); it("should return single origin for IPv6 localhost requests", () => { const mockRequestHttp = { headers: { get: (name: string) => name.toLowerCase() === "origin" ? "http://[::1]:3000" : null, }, } as unknown as Request; const corsOptionsHttp = getCorsOptionsForRequest(mockRequestHttp); expect(corsOptionsHttp.origin).toBe("http://[::1]"); expect(typeof corsOptionsHttp.origin).toBe("string"); const mockRequestHttps = { headers: { get: (name: string) => name.toLowerCase() === "origin" ? "https://[::1]:8443" : null, }, } as unknown as Request; const corsOptionsHttps = getCorsOptionsForRequest(mockRequestHttps); expect(corsOptionsHttps.origin).toBe("https://[::1]"); expect(typeof corsOptionsHttps.origin).toBe("string"); }); it("should normalize IPv6 localhost ports to base origin", () => { // Test different ports all normalize to the same base origin const ports = [3000, 8080, 8443, 5173]; for (const port of ports) { const mockRequest = { headers: { get: (name: string) => name.toLowerCase() === "origin" ? `http://[::1]:${port}` : null, }, } as unknown as Request; const corsOptions = getCorsOptionsForRequest(mockRequest); expect(corsOptions.origin).toBe("http://[::1]"); } }); it("should return wildcard for requests without Origin header", () => { const mockRequest = { headers: { get: () => null, }, } as unknown as Request; const corsOptions = getCorsOptionsForRequest(mockRequest); expect(corsOptions.origin).toBe("*"); }); it("should return wildcard for invalid origins", () => { const mockRequest = { headers: { get: (name: string) => name.toLowerCase() === "origin" ? "https://evil.com" : null, }, } as unknown as Request; const corsOptions = getCorsOptionsForRequest(mockRequest); expect(corsOptions.origin).toBe("*"); }); it("should include all CORS configuration", () => { const mockRequest = { headers: { get: (name: string) => name.toLowerCase() === "origin" ? "https://mcp.globalping.io" : null, }, } as unknown as Request; const corsOptions = getCorsOptionsForRequest(mockRequest); expect(corsOptions).toHaveProperty("methods"); expect(corsOptions).toHaveProperty("headers"); expect(corsOptions).toHaveProperty("exposeHeaders"); expect(corsOptions).toHaveProperty("maxAge"); expect(corsOptions.methods).toBe(CORS_CONFIG.METHODS); expect(corsOptions.headers).toBe(CORS_CONFIG.HEADERS); expect(corsOptions.exposeHeaders).toBe(CORS_CONFIG.EXPOSE_HEADERS); expect(corsOptions.maxAge).toBe(CORS_CONFIG.MAX_AGE); }); }); describe("validateHost", () => { describe("valid hosts", () => { it("should accept production hostnames", () => { expect(validateHost("mcp.globalping.io")).toBe(true); expect(validateHost("mcp.globalping.dev")).toBe(true); }); it("should accept localhost variants", () => { expect(validateHost("localhost")).toBe(true); expect(validateHost("127.0.0.1")).toBe(true); }); it("should accept hosts with port numbers", () => { expect(validateHost("localhost:3000")).toBe(true); expect(validateHost("localhost:8080")).toBe(true); expect(validateHost("127.0.0.1:3000")).toBe(true); expect(validateHost("mcp.globalping.io:443")).toBe(true); }); it("should be case-insensitive", () => { expect(validateHost("LOCALHOST")).toBe(true); expect(validateHost("MCP.GLOBALPING.IO")).toBe(true); expect(validateHost("Localhost:3000")).toBe(true); }); it("should strip port numbers before validation", () => { // Port should be stripped, so these should match base hostnames expect(validateHost("localhost:9999")).toBe(true); expect(validateHost("127.0.0.1:65535")).toBe(true); }); it("should accept IPv6 localhost without port", () => { expect(validateHost("[::1]")).toBe(true); }); it("should accept IPv6 localhost with port numbers", () => { expect(validateHost("[::1]:3000")).toBe(true); expect(validateHost("[::1]:8080")).toBe(true); expect(validateHost("[::1]:8787")).toBe(true); expect(validateHost("[::1]:8443")).toBe(true); }); it("should strip port from IPv6 addresses", () => { // IPv6 with various ports should all normalize to [::1] expect(validateHost("[::1]:9999")).toBe(true); expect(validateHost("[::1]:65535")).toBe(true); expect(validateHost("[::1]:80")).toBe(true); expect(validateHost("[::1]:443")).toBe(true); }); it("should handle IPv6 in various formats", () => { // Test that [::1] is properly recognized and validated expect(validateHost("[::1]")).toBe(true); // With scheme-like prefix (though Host header shouldn't have this, test robustness) expect(validateHost("[::1]:8080")).toBe(true); }); }); describe("invalid hosts", () => { it("should reject null host", () => { expect(validateHost(null)).toBe(false); }); it("should reject empty string host", () => { expect(validateHost("")).toBe(false); }); it("should reject malicious external hosts", () => { expect(validateHost("evil.com")).toBe(false); expect(validateHost("attacker.example.com")).toBe(false); expect(validateHost("malicious-site.com")).toBe(false); }); it("should reject hosts with wrong IP addresses", () => { expect(validateHost("192.168.1.1")).toBe(false); expect(validateHost("10.0.0.1")).toBe(false); expect(validateHost("172.16.0.1")).toBe(false); }); it("should reject non-localhost IPv6 addresses", () => { // Only [::1] should be accepted, other IPv6 addresses should be rejected expect(validateHost("[2001:db8::1]")).toBe(false); expect(validateHost("[fe80::1]")).toBe(false); expect(validateHost("[::ffff:192.0.2.1]")).toBe(false); expect(validateHost("[2001:db8::1]:8080")).toBe(false); }); it("should reject malformed IPv6 addresses", () => { // Missing brackets, incomplete addresses, etc. expect(validateHost("::1")).toBe(false); expect(validateHost("::1:8080")).toBe(false); expect(validateHost("[::1")).toBe(false); expect(validateHost("::1]")).toBe(false); expect(validateHost("[:1]")).toBe(false); }); it("should reject IPv6 addresses with protocol prefixes", () => { // Host headers should never include protocols expect(validateHost("http://[::1]")).toBe(false); expect(validateHost("https://[::1]")).toBe(false); expect(validateHost("http://[::1]:8080")).toBe(false); expect(validateHost("https://[::1]:8443")).toBe(false); }); it("should reject subdomain attacks", () => { expect(validateHost("evil.mcp.globalping.io")).toBe(false); expect(validateHost("subdomain.globalping.io")).toBe(false); }); it("should reject similar but different domains", () => { expect(validateHost("mcp.globalping.io.evil.com")).toBe(false); expect(validateHost("mcpglobalping.io")).toBe(false); expect(validateHost("mcp-globalping.io")).toBe(false); }); it("should reject localhost-like domains", () => { expect(validateHost("localhost.evil.com")).toBe(false); expect(validateHost("127.0.0.1.evil.com")).toBe(false); expect(validateHost("my-localhost.com")).toBe(false); }); }); describe("DNS rebinding attack prevention", () => { it("should prevent DNS rebinding with spoofed hosts", () => { // Attacker tries to use their domain as Host header expect(validateHost("attacker.com")).toBe(false); expect(validateHost("rebinding-attack.net")).toBe(false); }); it("should prevent homograph attacks in Host header", () => { // Using unicode characters that look similar expect(validateHost("mсp.globalping.io")).toBe(false); // Cyrillic 'с' }); it("should work with getAllowedHostnames extraction", () => { // Hostnames should be extracted from CORS_CONFIG.ALLOWED_ORIGINS // including protocol schemes like vscode:// and claude:// expect(validateHost("localhost")).toBe(true); expect(validateHost("127.0.0.1")).toBe(true); }); }); describe("port handling", () => { it("should strip standard HTTP port (80)", () => { expect(validateHost("localhost:80")).toBe(true); }); it("should strip standard HTTPS port (443)", () => { expect(validateHost("mcp.globalping.io:443")).toBe(true); }); it("should strip custom ports", () => { expect(validateHost("localhost:3000")).toBe(true); expect(validateHost("localhost:8080")).toBe(true); expect(validateHost("127.0.0.1:5000")).toBe(true); }); it("should handle malformed port numbers", () => { // If port is present but malformed, the hostname part should still be validated expect(validateHost("localhost:abc")).toBe(true); // Port stripped, localhost remains expect(validateHost("evil.com:80")).toBe(false); // Still evil after port strip }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jsdelivr/globalping-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server