{"record":{"id":"8fdd2f95c8f70da6","repo":"jackwener/OpenCLI","slug":"unsupported-image-url-protocol-parsed-protocol","errorCode":null,"errorMessage":"Unsupported image URL protocol: ${parsed.protocol}","messagePattern":"Unsupported image URL protocol: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/twitter/utils.js","lineNumber":93,"sourceCode":"    );\n}\n\n/**\n * Download a remote image to a per-call tmp directory. Returns the absolute\n * path on success. Caller owns the tmp dir and must clean it up. Throws\n * {@link ArgumentError} on bad input or download failure.\n *\n * @returns {Promise<{ absPath: string, cleanupDir: string }>}\n */\nexport async function downloadRemoteImage(imageUrl) {\n    let parsed;\n    try {\n        parsed = new URL(imageUrl);\n    } catch {\n        throw new ArgumentError(`Invalid image URL: ${imageUrl}`);\n    }\n    if (!/^https?:$/.test(parsed.protocol)) {\n        throw new ArgumentError(`Unsupported image URL protocol: ${parsed.protocol}`);\n    }\n    const response = await fetch(imageUrl);\n    if (!response.ok) {\n        throw new ArgumentError(`Image download failed: HTTP ${response.status}`);\n    }\n    const contentLength = Number(response.headers.get('content-length') || '0');\n    if (contentLength > MAX_IMAGE_SIZE_BYTES) {\n        throw new ArgumentError(`Image too large: ${(contentLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);\n    }\n    const ext = resolveImageExtension(imageUrl, response.headers.get('content-type'));\n    const cleanupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-twitter-'));\n    const absPath = path.join(cleanupDir, `image${ext}`);\n    const buffer = Buffer.from(await response.arrayBuffer());\n    if (buffer.byteLength > MAX_IMAGE_SIZE_BYTES) {\n        fs.rmSync(cleanupDir, { recursive: true, force: true });\n        throw new ArgumentError(`Image too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);\n    }\n    fs.writeFileSync(absPath, buffer);","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/utils.js#L75-L111","documentation":"downloadRemoteImage only accepts http: and https: URLs, enforced by the regex /^https?:$/. Any other protocol (ftp:, file:, data:, chrome:, etc.) throws ArgumentError. This blocks unsafe schemes like file:// that would read local resources or bypass intended network fetch semantics.","triggerScenarios":"Passing a data: URI, a file:// path, an ftp:// link, or a URL object whose protocol resolved to something non-HTTP — anything where parsed.protocol fails the http/https test.","commonSituations":"Trying to reuse a data:image/png;base64,... string from an HTML snippet; passing filesystem paths where a URL is expected; internal ftp mirrors; browser-extension style chrome:// image URLs.","solutions":["Convert data: URIs to a local file (Buffer.from(base64part, 'base64') written to disk) and pass the local path instead.","Use an https:// mirror of the image rather than ftp:// or file://.","If you have a local file, use the local-image code path (resolveImagePath), not downloadRemoteImage."],"exampleFix":"// before\nawait downloadRemoteImage('file:///home/me/cat.png');\n// after\nawait postWithImage('/home/me/cat.png'); // local path goes through resolveImagePath","handlingStrategy":"type-guard","validationCode":"const u = new URL(imageUrl);\nif (!/^https?:$/.test(u.protocol)) {\n  throw new Error(`Only http/https supported, got ${u.protocol}`);\n}","typeGuard":"function isHttpImageUrl(value) {\n  try { return /^https?:$/.test(new URL(value).protocol); } catch { return false; }\n}","tryCatchPattern":"try {\n  await downloadRemoteImage(imageUrl);\n} catch (err) {\n  if (err instanceof ArgumentError && /Unsupported image URL protocol/.test(err.message)) {\n    const local = await materializeToLocalFile(imageUrl); // data:/file: handlers\n    return postWithImage(local);\n  }\n  throw err;\n}","preventionTips":["Never pass data: or file: URIs to the remote downloader; write them to disk first.","Accept only http(s) links from upstream callers; validate at the boundary.","Convert base64 data URIs via Buffer.from(b64, 'base64') into temp files."],"tags":["url-parsing","security","input-validation"],"backgroundTag":"unsupported-url-protocol","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}