{"record":{"id":"2ffc697c4b6d221f","repo":"paperclipai/paperclip","slug":"teams-destination-contains-an-untrusted-service-ur","errorCode":null,"errorMessage":"Teams destination contains an untrusted service URL","messagePattern":"Teams destination contains an untrusted service URL","errorType":"validation","errorClass":"TeamsServiceUrlValidationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-sdk-runtime.ts","lineNumber":991,"sourceCode":"function trustedTeamsServiceUrl(\n  value: unknown,\n  configuredApiUrl: string | null,\n): string {\n  const rawValue = typeof value === \"string\" ? value : \"\";\n  const normalized = normalizedTeamsServiceUrl(value);\n  const parsed = new URL(normalized);\n  const rawParsed = new URL(rawValue);\n  const officialHost =\n    rawValue === rawValue.trim() &&\n    parsed.port === \"\" &&\n    isOfficialTeamsConnectorHost(parsed.hostname) &&\n    // Encoded path bytes can be normalized away by URL parsing. Microsoft\n    // connector base URLs do not require them, so reject them before applying\n    // the canonical one-segment path policy.\n    !/%[0-9a-f]{2}/i.test(rawValue) &&\n    isCanonicalTeamsConnectorPath(rawParsed.pathname);\n  if (officialHost || normalized === configuredApiUrl) return normalized;\n  throw new TeamsServiceUrlValidationError(\n    \"Teams destination contains an untrusted service URL\",\n  );\n}\n\n/**\n * Microsoft binds serviceUrl into the authenticated Bot Connector JWT and\n * requires replies to target that matching URL. The URL is mutable routing\n * state, not conversation identity, so current thread ids omit it. Persist the\n * latest verified route under the stable conversation id and scope each\n * outbound call to a fresh API client rooted at that route. Legacy thread ids\n * that embedded a URL remain readable, but a newer persisted route wins. A\n * context-local getter keeps simultaneous conversations isolated without\n * forcing unrelated Teams threads through a single network queue.\n */\nexport function scopeMicrosoftTeamsEgress(\n  adapter: Adapter,\n  configuredApiUrl?: string,\n  enableFileConsent = false,","sourceCodeStart":973,"sourceCodeEnd":1009,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-sdk-runtime.ts#L973-L1009","documentation":"After shape checks, normalizedTeamsServiceUrl applies a trust policy: the normalized URL is accepted only if its host is an official Microsoft connector host (without percent-encoded path bytes, enforcing a canonical one-segment path) or if it equals the configured API URL. Anything else throws TeamsServiceUrlValidationError with 'Teams destination contains an untrusted service URL' to prevent SSRF via attacker-supplied serviceUrl values.","triggerScenarios":"A Teams activity/destination carries a serviceUrl pointing at a non-Microsoft host (e.g. attacker-controlled server), contains percent-encoded path bytes like %2f, or has a non-canonical path segment pattern that fails isCanonicalTeamsConnectorPath, and it does not match the configured API URL.","commonSituations":"Security probing where a crafted Bot Framework activity injects a malicious serviceUrl; proxy or gateway URLs substituted for the real connector host; encoded characters sneaked into the connector path.","solutions":["Use only Microsoft Bot Connector serviceUrl hosts (e.g. smba.trafficmanager.net, webchat.botframework.com) exactly as issued in the authenticated activity.","Remove percent-encoding from the path; Microsoft connector base URLs do not need encoded bytes.","If you intentionally target a private/gateway endpoint, configure it as the trusted configured API URL that normalization compares against.","Treat repeated occurrences as a security signal: validate the activity's serviceUrl against the Bot Connector JWT-issued value before ingestion."],"exampleFix":"// before\nserviceUrl: \"https://evil.example.com/v3\"\n// after\nserviceUrl: \"https://smba.trafficmanager.net/americas\"","handlingStrategy":"try-catch","validationCode":"const allowedHosts = [/\\.trafficmanager\\.net$/, /botframework\\.com$/];\nconst u = new URL(dest.serviceUrl);\nif (!allowedHosts.some(r => r.test(u.hostname)) && dest.serviceUrl !== configuredApiUrl) {\n  throw new Error(\"serviceUrl is not a trusted Teams connector URL\");\n}","typeGuard":"function isTrustedTeamsServiceUrl(value: string, configuredApiUrl: string): boolean {\n  return value === configuredApiUrl || /\\.trafficmanager\\.net$|\\.botframework\\.com$/.test(new URL(value).hostname);\n}","tryCatchPattern":"try {\n  const url = normalizedTeamsServiceUrl(dest.serviceUrl);\n} catch (e) {\n  if (e instanceof TeamsServiceUrlValidationError && e.message.includes(\"untrusted\")) {\n    // treat as potential spoofed activity: drop it and alert, never call the URL\n  }\n}","preventionTips":["Never trust serviceUrl from unauthenticated payloads; verify the Bot Connector JWT first.","Only accept Microsoft connector hosts or your configured API URL.","Treat untrusted-serviceUrl errors as security events worth logging/alerting.","Avoid percent-encoded path bytes in connector URLs; validate paths before use."],"tags":["teams","ssrf","security","url-validation"],"backgroundTag":"invalid-url","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}