T09 · Insecure Skill Coding Practices
- Location
lib/cairn/fetch.js:4- Finding
Arbitrary Web Ingestion Permits Server-Side Request Forgery
- Content
View full analysis
{ if (isOffline()) { throw new Error(`cairn: web fetch blocked by CAIRN_OFFLINE (would have fetched ${url}). ` + `Unset CAIRN_OFFLINE to allow, or skip web ingestion.`); } const res = await fetch(url, { headers: { 'user-agent': UA } }); if (!res.ok) throw new Error(`fetch ${url}: ${res.status} ${res.statusText}`); const ct = res.headers.get('content-type') ?? ''; const body = await res.text(); if (ct.startsWith('text/html')) return extractHtml(body); if (ct.startsWith('text/')) return { title: null, text: normalize(body) }; throw new Error(`fetch ${url}: unsupported content-type ${ct}`); }; ``` The MCP input is classified as a web source based only on its string prefix: ```js const autoDetectKind = (target) => { if (target.startsWith('http://') || target.startsWith('https://')) return 'web'; // ... }; ``` It is then passed to the ingestion provider without destination restrictions: ```js server.registerTool('add', { description: 'Add a new source to the index. Auto-detects kind when unspecified: URLs → web, directories → code, *.pdf → pdf, other files → file.', inputSchema: { kind: z .enum(['web', 'code', 'file', 'text', 'pdf']) .optional() .describe('Source kind. Default: auto-detect from url/path.'), target: z.string().describe('URL (for web) or absolute path (for code/file/pdf).'), label: z.string().optional().describe('Optional human-readable label.'), include: z .array(z.string()) .optional() .describe('Suffix matches for code kind (e.g. [".ts", ".tsx"] ...[truncated 2889 chars]- Remediation
View remediation
