{"record":{"id":"a6427a2287ac18b1","repo":"hcengineering/platform","slug":"cannot-process-provided-link","errorCode":null,"errorMessage":"Cannot process provided link","messagePattern":"Cannot process provided link","errorType":"http","errorClass":"ApiError","httpStatus":403,"severity":"error","filePath":"services/print/pod-print/src/server.ts","lineNumber":214,"sourceCode":"  app.use(cors())\n  app.use(express.json())\n  app.use(withMeasureContext({ ctx: measureCtx }))\n\n  app.get(\n    '/print',\n    wrapRequest(async (req, res, wsIds, wsLoginInfo) => {\n      const ctx = req.ctx\n      const rawlink = req.query.link as string\n      const link = decodeURIComponent(rawlink)\n\n      // Verify that link is from the same host and protocol is among the allowed\n      const url = new URL(link)\n      if (\n        !['http:', 'https:'].includes(url.protocol) ||\n        (whitelistedHostnames != null && !whitelistedHostnames.has(url.hostname))\n      ) {\n        ctx.error('Rejected processing unexpected link', { link })\n        throw new ApiError(403, 'Cannot process provided link')\n      }\n\n      const options = parsePrintOptions(req.query)\n\n      const printRes = await ctx.with(\n        'print',\n        { kind: options.kind, orientation: options.orientation },\n        (ctx) => print(ctx, link, options),\n        {\n          url,\n          viewport: options.viewport\n        }\n      )\n\n      if (printRes === undefined) {\n        throw new ApiError(400, 'Failed to print')\n      }\n","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/print/pod-print/src/server.ts#L196-L232","documentation":"The /print endpoint only renders links whose protocol is http: or https: and, when a hostname whitelist is configured for the server (allowedHostnames), whose hostname is in that whitelist. Any other link is rejected with this 403 ApiError. This is an SSRF / access-control guard, not a formatting problem.","triggerScenarios":"GET /print?link=file:///etc/passwd or link=javascript:... (bad protocol); or an http(s) link whose hostname is not in the server's allowedHostnames list; also a hostname that differs in case/subdomain from the whitelisted entry (Set lookup is exact).","commonSituations":"Deploying the pod with allowedHostnames configured but the app generating links with a different domain (e.g. internal k8s service name vs public domain); printing localhost links that were never whitelisted; client sending encoded URIs whose decoded hostname doesn't match.","solutions":["Use an https:// (or http://) URL for the link parameter.","Ask the operator to add your link's hostname to the pod's allowedHostnames configuration, or run the pod with an empty whitelist (whitelist disabled) if appropriate.","Verify exact hostname match — the check compares url.hostname against the Set exactly (no wildcards, no case folding).","Make sure the link is properly encoded once, not double-encoded, so the decoded hostname is the intended one."],"exampleFix":"// before\nfetch(`/print?link=${encodeURIComponent('file:///tmp/doc.html')}`)\n// after\nconst target = 'https://docs.example.com/report'\nfetch(`/print?link=${encodeURIComponent(target)}`) // https:// and whitelisted hostname","handlingStrategy":"validation","validationCode":"function assertPrintableUrl (raw: string, allowedHostnames?: string[]): URL {\n  const url = new URL(decodeURIComponent(raw))\n  if (!['http:', 'https:'].includes(url.protocol)) {\n    throw new Error(`protocol not allowed: ${url.protocol}`)\n  }\n  if (allowedHostnames != null && allowedHostnames.length > 0 && !allowedHostnames.includes(url.hostname)) {\n    throw new Error(`hostname not whitelisted: ${url.hostname}`)\n  }\n  return url\n}","typeGuard":"function isHttpUrl (v: unknown): v is URL {\n  try { const u = new URL(String(v)); return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false }\n}","tryCatchPattern":"try {\n  const res = await fetch(`/print?link=${encodeURIComponent(link)}`)\n  if (res.status === 403) {\n    const body = await res.json()\n    throw new Error(`Link rejected by print service (403): ${body.message} — check protocol and hostname whitelist`)\n  }\n  return await res.json()\n} catch (err) { /* handle */ }","preventionTips":["Only send http(s) links; never file:, data:, or javascript: URIs.","Confirm your target hostname is in the pod's allowedHostnames before deploying.","Remember matching is exact — no wildcards or subdomain inference; encode the link exactly once.","Keep an ops checklist mapping environments to whitelisted domains."],"tags":["security","ssrf","whitelist","http-403"],"backgroundTag":"url-not-allowed","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}