{"record":{"id":"43a5c0e1a42f7695","repo":"schollz/croc","slug":"stored-transfer-origin-must-contain-only-an-https","errorCode":null,"errorMessage":"Stored-transfer origin must contain only an HTTPS scheme and host","messagePattern":"Stored-transfer origin must contain only an HTTPS scheme and host","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":142,"sourceCode":"  }\n}\n\nfunction normalizeOrigin(value: string) {\n  const parsed = new URL(value);\n  const loopback =\n    parsed.hostname === \"localhost\" ||\n    parsed.hostname === \"127.0.0.1\" ||\n    parsed.hostname === \"[::1]\" ||\n    parsed.hostname === \"::1\";\n  if (\n    (parsed.protocol !== \"https:\" && !(parsed.protocol === \"http:\" && loopback)) ||\n    parsed.username ||\n    parsed.password ||\n    (parsed.pathname !== \"/\" && parsed.pathname !== \"\") ||\n    parsed.search ||\n    parsed.hash\n  ) {\n    throw new Error(\"Stored-transfer origin must contain only an HTTPS scheme and host\");\n  }\n  return parsed.origin;\n}\n\nfunction validateShare(share: StoredShare) {\n  if (!/^[A-Za-z0-9_-]{22}$/.test(share.id)) {\n    throw new Error(\"Invalid stored-transfer id\");\n  }\n  if (share.key.byteLength !== storedKeyBytes) {\n    throw new Error(\"Invalid stored-transfer key\");\n  }\n  share.origin = normalizeOrigin(share.origin);\n  return share;\n}\n\nexport function formatStoredBrowserURL(share: StoredShare) {\n  validateShare(share);\n  return `${share.origin}/s/${share.id}#v1.${base64URL(share.key)}`;","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L124-L160","documentation":"normalizeOrigin() enforces that a stored share's origin is a bare HTTPS origin (or HTTP on localhost/127.0.0.1/[::1]) with no username/password, path, query, or fragment. Anything else — a path suffix, credentials, ftp/file scheme, or plain HTTP on a non-loopback host — throws, because the origin is embedded in share URLs/CLI tokens and used as the trust anchor for later fetches.","triggerScenarios":"validateShare() runs on every parseStoredShare/formatStored* call and on createStoredUpload's return (origin: window.location.origin). A share token whose encoded origin is e.g. \"https://host/croc\" or \"http://192.168.1.5\" throws; likewise running the web UI itself over plain HTTP on a LAN IP, since window.location.origin then fails the check.","commonSituations":"Self-hosting the web client on an internal network over HTTP (not localhost) — every created share fails validation; a reverse proxy that adds a path prefix; hand-edited or truncated share tokens where the origin segment decodes to garbage.","solutions":["Serve the web client over HTTPS (or on localhost/127.0.0.1) so window.location.origin passes","Strip any path/query from the origin embedded in tokens — only scheme://host[:port] is allowed","Re-copy the share token from the sender unedited; do not paste it through URL-shorteners or re-host it under a subpath"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const isValidOrigin = (value: string): boolean => { try { const u = new URL(value); const loopback = [\"localhost\",\"127.0.0.1\",\"[::1]\",\"::1\"].includes(u.hostname); return (u.protocol === \"https:\" || (u.protocol === \"http:\" && loopback)) && !u.username && !u.password && (u.pathname === \"/\" || u.pathname === \"\") && !u.search && !u.hash; } catch { return false; } };","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Serve the croc web client over HTTPS (or localhost) so window.location.origin always validates","Never embed paths or credentials in share origins; the origin is scheme://host[:port] only"],"tags":["validation","url","https","origin","configuration"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}