{"record":{"id":"eb833589beee8af6","repo":"Budibase/budibase","slug":"the-plugin-origin-must-be-from-github","errorCode":null,"errorMessage":"The plugin origin must be from Github","messagePattern":"The plugin origin must be from Github","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/plugin/github.ts","lineNumber":17,"sourceCode":"import { utils as coreUtils } from \"@budibase/backend-core\"\nimport {\n  deleteFolderFileSystem,\n  getPluginMetadata,\n} from \"../../../utilities/fileSystem\"\nimport { downloadUnzipTarball } from \"./utils\"\n\nfunction parseGithubUrl(url: string): URL {\n  let parsed: URL\n  try {\n    parsed = new URL(url)\n  } catch {\n    throw new Error(\"Invalid Github URL\")\n  }\n\n  if (parsed.protocol !== \"https:\" || parsed.hostname !== \"github.com\") {\n    throw new Error(\"The plugin origin must be from Github\")\n  }\n\n  return parsed\n}\n\nexport async function request(\n  url: string,\n  headers: Record<string, string> = {},\n  err: string\n) {\n  const response = await coreUtils.fetchWithBlacklist(url, { headers })\n  if (response.status >= 300) {\n    const respErr = await response.text()\n    throw new Error(`Error: ${err} - ${respErr}`)\n  }\n  return response.json()\n}\n","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/plugin/github.ts#L1-L35","documentation":"Budibase plugin installation only accepts plugin sources hosted on github.com. parseGithubUrl parses the supplied URL and rejects anything whose protocol is not https: or whose hostname is not exactly 'github.com'. This enforces a security whitelist so plugin code cannot be pulled from arbitrary hosts.","triggerScenarios":"Calling githubUpload (via the plugin create API with source=GITHUB) with a URL using http:// instead of https://, or a hostname other than github.com such as gist.github.com, gitlab.com, raw.githubusercontent.com, or an enterprise GitHub host.","commonSituations":"Pasting a raw.githubusercontent.com or codeload URL instead of the repo URL; using an on-prem GitHub Enterprise instance; linking to GitLab/Bitbucket repos; copying an http:// link from old docs.","solutions":["Use the standard https://github.com/owner/repo URL of the plugin repository","Strip sub-paths like /blob/... or raw file URLs and point at the repository root","Replace http:// with https://","Host the plugin on github.com or switch the source to NPM if applicable"],"exampleFix":"// before\nawait installPlugin({ source: 'GITHUB', url: 'http://github.com/org/plugin' })\n// after\nawait installPlugin({ source: 'GITHUB', url: 'https://github.com/org/plugin' })","handlingStrategy":"validation","validationCode":"const u = new URL(url)\nif (u.protocol !== 'https:' || u.hostname !== 'github.com') {\n  throw new Error('Use a https://github.com/owner/repo URL')\n}","typeGuard":"function isGithubRepoUrl(url: string): boolean {\n  try {\n    const u = new URL(url)\n    return u.protocol === 'https:' && u.hostname === 'github.com'\n  } catch {\n    return false\n  }\n}","tryCatchPattern":"try {\n  await installPlugin({ source: 'GITHUB', url })\n} catch (err) {\n  if (err.message === 'The plugin origin must be from Github') {\n    // surface a UI hint to correct the URL to github.com\n  }\n}","preventionTips":["Always submit the repository root URL https://github.com/owner/repo","Avoid raw.githubusercontent.com, codeload, gist, or GHE hostnames","Prefer https over http and strip .git suffixes carefully (the code handles .git but only after an initial valid parse)"],"tags":["validation","url","plugin"],"backgroundTag":"plugin-origin-not-allowed","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}