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:
parent
219b4b1605
commit
4d9ba1ea0e
2 changed files with 7 additions and 40 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue