pdf-server: remove domain allowlist, require HTTPS only (#487)

Replace the hardcoded list of allowed remote origins with a simple
HTTPS protocol check. Any HTTPS URL is now accepted for remote PDFs.
Local file allowlisting remains unchanged.
This commit is contained in:
Olivier Chafik 2026-02-23 13:27:15 -08:00 • committed by GitHub
commit 4d9ba1ea0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 7 additions and 40 deletions

View file

@ -19,7 +19,6 @@ import {
pathToFileUrl, pathToFileUrl,
fileUrlToPath, fileUrlToPath,
allowedLocalFiles, allowedLocalFiles,
allowedRemoteOrigins,
DEFAULT_PDF, DEFAULT_PDF,
} from "./server.js"; } from "./server.js";
@ -132,9 +131,6 @@ async function main() {
} }
console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`); console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`);
console.error(
`[pdf-server] Allowed origins: ${[...allowedRemoteOrigins].join(", ")}`,
);
if (stdio) { if (stdio) {
await startStdioServer(createServer); await startStdioServer(createServer);

View file

@ -2,7 +2,7 @@
* PDF MCP Server * PDF MCP Server
* *
* An MCP server that displays PDFs in an interactive viewer. * An MCP server that displays PDFs in an interactive viewer.
* Supports local files and remote URLs from academic sources (arxiv, biorxiv, etc). * Supports local files and remote HTTPS URLs.
* *
* Tools: * Tools:
* - list_pdfs: List available PDFs * - list_pdfs: List available PDFs
@ -44,26 +44,6 @@ export const CACHE_MAX_LIFETIME_MS = 60_000; // 60 seconds
/** Max size for cached PDFs (defensive limit to prevent memory exhaustion) */ /** Max size for cached PDFs (defensive limit to prevent memory exhaustion) */
export const CACHE_MAX_PDF_SIZE_BYTES = 50 * 1024 * 1024; // 50MB export const CACHE_MAX_PDF_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
/** Allowed remote origins (security allowlist) */
export const allowedRemoteOrigins = new Set([
"https://agrirxiv.org",
"https://arxiv.org",
"https://chemrxiv.org",
"https://edarxiv.org",
"https://engrxiv.org",
"https://hal.science",
"https://osf.io",
"https://psyarxiv.com",
"https://ssrn.com",
"https://www.biorxiv.org",
"https://www.eartharxiv.org",
"https://www.medrxiv.org",
"https://www.preprints.org",
"https://www.researchsquare.com",
"https://www.sportarxiv.org",
"https://zenodo.org",
]);
/** Allowed local file paths (populated from CLI args) */ /** Allowed local file paths (populated from CLI args) */
export const allowedLocalFiles = new Set<string>(); export const allowedLocalFiles = new Set<string>();
@ -134,14 +114,11 @@ export function validateUrl(url: string): { valid: boolean; error?: string } {
return { valid: true }; return { valid: true };
} }
// Remote URL - check against allowed origins // Remote URL - require HTTPS
try { try {
const parsed = new URL(url); const parsed = new URL(url);
const origin = `${parsed.protocol}//${parsed.hostname}`; if (parsed.protocol !== "https:") {
if ( return { valid: false, error: `Only HTTPS URLs are allowed: ${url}` };
![...allowedRemoteOrigins].some((allowed) => origin.startsWith(allowed))
) {
return { valid: false, error: `Origin not allowed: ${origin}` };
} }
return { valid: true }; return { valid: true };
} catch { } catch {
@ -417,7 +394,7 @@ export function createServer(): McpServer {
// Create session-local cache (isolated per server instance) // Create session-local cache (isolated per server instance)
const { readPdfRange } = createPdfCache(); const { readPdfRange } = createPdfCache();
// Tool: list_pdfs - List available PDFs (local files + allowed origins) // Tool: list_pdfs - List available PDFs
server.tool( server.tool(
"list_pdfs", "list_pdfs",
"List available PDFs that can be displayed", "List available PDFs that can be displayed",
@ -443,7 +420,7 @@ export function createServer(): McpServer {
); );
} }
parts.push( parts.push(
`Remote PDFs from ${[...allowedRemoteOrigins].join(", ")} can also be loaded dynamically.`, `Any remote PDF accessible via HTTPS can also be loaded dynamically.`,
); );
return { return {
@ -451,7 +428,6 @@ export function createServer(): McpServer {
structuredContent: { structuredContent: {
localFiles: pdfs.filter((p) => p.type === "local").map((p) => p.url), localFiles: pdfs.filter((p) => p.type === "local").map((p) => p.url),
allowedDirectories: [...allowedLocalDirs], allowedDirectories: [...allowedLocalDirs],
allowedOrigins: [...allowedRemoteOrigins],
}, },
}; };
}, },
@ -531,11 +507,6 @@ export function createServer(): McpServer {
}, },
); );
// Build allowed domains list for tool description (strip https:// and www.)
const allowedDomains = [...allowedRemoteOrigins]
.map((origin) => origin.replace(/^https?:\/\/(www\.)?/, ""))
.join(", ");
// Tool: display_pdf - Show interactive viewer // Tool: display_pdf - Show interactive viewer
registerAppTool( registerAppTool(
server, server,
@ -547,7 +518,7 @@ export function createServer(): McpServer {
Accepts: Accepts:
- Local files explicitly added to the server (use list_pdfs to see available files) - Local files explicitly added to the server (use list_pdfs to see available files)
- Local files under directories provided by the client as MCP roots - Local files under directories provided by the client as MCP roots
- Remote PDFs from: ${allowedDomains}`, - Any remote PDF accessible via HTTPS`,
inputSchema: { inputSchema: {
url: z.string().default(DEFAULT_PDF).describe("PDF URL"), url: z.string().default(DEFAULT_PDF).describe("PDF URL"),
page: z.number().min(1).default(1).describe("Initial page"), page: z.number().min(1).default(1).describe("Initial page"),