{"record":{"id":"f2efba36178d3f32","repo":"agalwood/Motrix","slug":"plugin-http-scheme-not-allowed","errorCode":"plugin.http.scheme_not_allowed","errorMessage":"URL scheme '${parsed.protocol}' is not allowed; use http: or https:","messagePattern":"URL scheme '(.+?)' is not allowed; use http: or https:","errorType":"validation","errorClass":"HttpError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/http.ts","lineNumber":142,"sourceCode":"// Shared dispatcher for non-proxied requests; per-call ProxyAgent is built\n// fresh when `opts.proxy` is provided.\nconst sharedAgent = new Agent()\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction parseUrl(raw: string): URL {\n  try {\n    return new URL(raw)\n  } catch {\n    throw new HttpError('plugin.http.invalid_url', `Invalid URL: ${raw}`)\n  }\n}\n\nfunction checkScheme(parsed: URL): void {\n  if (!ALLOWED_SCHEMES.has(parsed.protocol)) {\n    throw new HttpError(\n      'plugin.http.scheme_not_allowed',\n      `URL scheme '${parsed.protocol}' is not allowed; use http: or https:`\n    )\n  }\n}\n\nfunction clampTimeout(ms: number | undefined, defaultMs: number): number {\n  // Reject non-finite values (NaN/Infinity) — http.get/post reach this with\n  // unvalidated opts, and Math.min(MAX, NaN) is NaN, disabling the timeout.\n  if (ms === undefined || !Number.isFinite(ms)) return defaultMs\n  return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, ms))\n}\n\nfunction clampMaxBody(\n  requested: number | undefined,\n  defaultBytes: number\n): number {\n  // Only accept a finite, positive request. A NaN (Math.min(NaN, HARD) = NaN)","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/http.ts#L124-L160","documentation":"The HTTP capability only permits http: and https: URLs. parseUrl() accepts any syntactically valid URL (including file:, ftp:, data:), then checkScheme() rejects anything not in ALLOWED_SCHEMES. This is an SSRF / local-file-access guard around plugin-initiated network calls, and it is re-applied on every redirect hop so a Location response cannot escape the allowlist.","triggerScenarios":"Calling http.get/post/request with a URL whose protocol is not http:/https: (e.g. 'file:///etc/passwd', 'ftp://host', 'data:text/plain,x'). Also triggered when a 3xx response's Location header points to a non-allowed scheme, because checkScheme() runs on every hop at http.ts:412.","commonSituations":"Plugin builds a URL from user/config input without normalizing; plugin reads a local-resource path formatted as file://; CI/runtime upgrades tighten the allowlist; a server or short-link service redirects to a non-http scheme.","solutions":["Normalize URLs to http:// or https:// before calling the http capability.","For local files, use the fs/storage capability instead of file:// over http.","Sanitize untrusted URL input (strip/replace non-http protocols) at the plugin boundary.","If a redirect target is the cause, set redirect:'manual' and inspect each Location yourself."],"exampleFix":"// before\nawait http.request({ url: 'file:///etc/passwd', responseType: 'text' })\n\n// after\nawait http.request({ url: 'https://example.com/data', responseType: 'text' })","handlingStrategy":"validation","validationCode":"import { ALLOWED_SCHEMES } from '...'\nfunction assertHttpUrl(raw: string): URL {\n  const u = new URL(raw)\n  if (u.protocol !== 'http:' && u.protocol !== 'https:') {\n    throw new Error(`refusing non-http(s) URL: ${u.protocol}`)\n  }\n  return u\n}\n// at call site:\nassertHttpUrl(url)\nawait http.request({ url, responseType: 'json' })","typeGuard":"function isHttpUrl(raw: string): raw is `${'http'|'https'}://${string}` {\n  try {\n    const u = new URL(raw)\n    return u.protocol === 'http:' || u.protocol === 'https:'\n  } catch { return false }\n}","tryCatchPattern":"try {\n  await http.request({ url, responseType: 'json' })\n} catch (e) {\n  if (e instanceof HttpError && e.code === 'plugin.http.scheme_not_allowed') {\n    // log and reject the untrusted input; do not retry as-is\n  } else throw e\n}","preventionTips":["Treat URLs from untrusted input as data, parse and validate the scheme before use.","Never construct file:// or ftp:// URLs for the http capability; use fs for local I/O.","When following redirects manually, re-run scheme validation on each Location."],"tags":["http","security","ssrf","url-validation","redirect"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}