paperclipai/paperclip · error · TeamsServiceUrlValidationError

Teams destination contains an untrusted service URL

Error message

Teams destination contains an untrusted service URL

What it means

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.

Source

Thrown at server/src/services/chat-sdk-runtime.ts:991

function trustedTeamsServiceUrl(
  value: unknown,
  configuredApiUrl: string | null,
): string {
  const rawValue = typeof value === "string" ? value : "";
  const normalized = normalizedTeamsServiceUrl(value);
  const parsed = new URL(normalized);
  const rawParsed = new URL(rawValue);
  const officialHost =
    rawValue === rawValue.trim() &&
    parsed.port === "" &&
    isOfficialTeamsConnectorHost(parsed.hostname) &&
    // Encoded path bytes can be normalized away by URL parsing. Microsoft
    // connector base URLs do not require them, so reject them before applying
    // the canonical one-segment path policy.
    !/%[0-9a-f]{2}/i.test(rawValue) &&
    isCanonicalTeamsConnectorPath(rawParsed.pathname);
  if (officialHost || normalized === configuredApiUrl) return normalized;
  throw new TeamsServiceUrlValidationError(
    "Teams destination contains an untrusted service URL",
  );
}

/**
 * Microsoft binds serviceUrl into the authenticated Bot Connector JWT and
 * requires replies to target that matching URL. The URL is mutable routing
 * state, not conversation identity, so current thread ids omit it. Persist the
 * latest verified route under the stable conversation id and scope each
 * outbound call to a fresh API client rooted at that route. Legacy thread ids
 * that embedded a URL remain readable, but a newer persisted route wins. A
 * context-local getter keeps simultaneous conversations isolated without
 * forcing unrelated Teams threads through a single network queue.
 */
export function scopeMicrosoftTeamsEgress(
  adapter: Adapter,
  configuredApiUrl?: string,
  enableFileConsent = false,

View on GitHub (pinned to 01ad858492)

Solutions

  1. Use only Microsoft Bot Connector serviceUrl hosts (e.g. smba.trafficmanager.net, webchat.botframework.com) exactly as issued in the authenticated activity.
  2. Remove percent-encoding from the path; Microsoft connector base URLs do not need encoded bytes.
  3. If you intentionally target a private/gateway endpoint, configure it as the trusted configured API URL that normalization compares against.
  4. Treat repeated occurrences as a security signal: validate the activity's serviceUrl against the Bot Connector JWT-issued value before ingestion.

Example fix

// before
serviceUrl: "https://evil.example.com/v3"
// after
serviceUrl: "https://smba.trafficmanager.net/americas"
Defensive patterns

Strategy: try-catch

Validate before calling

const allowedHosts = [/\.trafficmanager\.net$/, /botframework\.com$/];
const u = new URL(dest.serviceUrl);
if (!allowedHosts.some(r => r.test(u.hostname)) && dest.serviceUrl !== configuredApiUrl) {
  throw new Error("serviceUrl is not a trusted Teams connector URL");
}

Type guard

function isTrustedTeamsServiceUrl(value: string, configuredApiUrl: string): boolean {
  return value === configuredApiUrl || /\.trafficmanager\.net$|\.botframework\.com$/.test(new URL(value).hostname);
}

Try / catch

try {
  const url = normalizedTeamsServiceUrl(dest.serviceUrl);
} catch (e) {
  if (e instanceof TeamsServiceUrlValidationError && e.message.includes("untrusted")) {
    // treat as potential spoofed activity: drop it and alert, never call the URL
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/2ffc697c4b6d221f. Report an issue: GitHub.