#10902: fix(msteams): fix inline pasted image downloads
channel: msteams
size: S
Cluster:
Discord and MS Teams Fixes
Fixes #5448
## Problem
Inline pasted images in MS Teams (clipboard paste, not drag-and-drop file attachments) were silently dropped, and the gateway never received the image bytes.
## Root Cause
Four independent bugs in the MSTeams extension's media download pipeline:
### 1. Bot Framework attachment URLs return JSON metadata, not binary content
Teams sends pasted images as attachments with Bot Framework URLs like:
```
https://smba.trafficmanager.net/.../v3/attachments/{id}
```
Fetching that bare URL returns a JSON metadata envelope. The binary lives at `/views/original`:
```
https://smba.trafficmanager.net/.../v3/attachments/{id}/views/original
```
**Fix:** Added `isBotFrameworkAttachmentUrl()` regex helper and `/views/original` suffix in `download.ts`. Trailing slashes are stripped before appending.
### 2. `trafficmanager.net` missing from auth host allowlist
`fetchWithAuthFallback` checks `DEFAULT_MEDIA_AUTH_HOST_ALLOWLIST` before retrying with a Bearer token on 401 responses. `trafficmanager.net` (used by `smba.trafficmanager.net` Bot Framework service URLs) was not listed, so auth fallback was silently skipped.
**Fix:** Added `trafficmanager.net` to `DEFAULT_MEDIA_AUTH_HOST_ALLOWLIST` in `shared.ts`.
### 3. Inbound media gate required ALL attachments to be `text/html`
The HTML-attachment gate used `every()`, requiring all attachments to be `text/html` before triggering the Graph API fallback (Tier 2). Teams sends mixed types (`image/*` + `text/html`) for pasted images, so the condition never matched.
**Fix:** Changed `every()` → `some()` and renamed `onlyHtmlAttachments` → `hasHtmlAttachments` in `inbound-media.ts`.
### 4. Graph API `hostedContents` collection returns `contentBytes: null`
`downloadGraphHostedContent` only read `contentBytes` from the collection response, which is [always null per Microsoft docs](https://learn.microsoft.com/en-us/graph/api/chatmessagehostedcontent-get?view=graph-rest-1.0&tabs=http):
> **Note:** `contentBytes` and `contentType` are always set to null.
The binary is available via the individual [`$value` endpoint](https://learn.microsoft.com/en-us/graph/api/chatmessagehostedcontent-get?view=graph-rest-1.0&tabs=http#example-2-get-hosted-content-bytes-for-an-image):
```
GET .../hostedContents/{id}/$value → 200 OK, content-type: image/png, <binary body>
```
**Fix:** Added `$value` endpoint fetch with Bearer auth as fallback when `contentBytes` is null in `graph.ts`. Kept the `contentBytes` fast path in case Microsoft starts populating it in the future.
## Changes
| File | Change |
|------|--------|
| `extensions/msteams/src/attachments/download.ts` | `isBotFrameworkAttachmentUrl()` + `/views/original` URL rewriting |
| `extensions/msteams/src/attachments/shared.ts` | `trafficmanager.net` added to auth allowlist |
| `extensions/msteams/src/attachments/graph.ts` | `$value` endpoint fetch for hosted content |
| `extensions/msteams/src/monitor-handler/inbound-media.ts` | `every()` → `some()` for HTML gate |
| `extensions/msteams/src/attachments.test.ts` | 4 new tests + updated existing tests |
| `CHANGELOG.md` | Fix entry |
## Testing
- **Unit tests:** 22/22 pass, including 4 new tests:
- `appends /views/original to Bot Framework attachment URLs`
- `uses auth fallback for Bot Framework 401 responses`
- `downloads hostedContents images via $value endpoint`
- `skips hosted content when $value fetch fails`
- **Live test:** Pasted image in Teams chat → gateway downloaded (2025×1525 PNG), resized, and dispatched to agent successfully
- **Gate:** `pnpm build && pnpm check && pnpm test` all pass
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
- Fixes MSTeams inline pasted image handling by rewriting Bot Framework attachment URLs to fetch binary bytes via `/views/original`.
- Expands the media auth allowlist to include `trafficmanager.net`, enabling Bearer-token retry when Bot Framework URLs return 401/403.
- Adjusts inbound media Tier-2 (Graph) fallback gating to trigger when *any* `text/html` attachment is present (mixed attachment types).
- Updates Graph hosted content download to fetch bytes from `hostedContents/{id}/$value` when collection `contentBytes` is null, with accompanying unit tests.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk.
- Changes are narrowly scoped to MS Teams media download paths, include targeted unit tests for each regression, and preserve existing behavior via fast paths and allowlist checks.
- No files require special attention
<!-- greptile_other_comments_section -->
<sub>(4/5) You can add custom instructions or style guidelines for the agent [here](https://app.greptile.com/review/github)!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#18716: msteams: fix DM image delivery + user target routing
by ktsushilofficial · 2026-02-17
82.7%
#20913: fix: intercept Discord embed images to enforce mediaMaxMb
by MumuTW · 2026-02-19
79.8%
#13089: fix(msteams): alias team config under channel conversation IDs for ...
by BradGroux · 2026-02-10
79.6%
#8964: test(msteams): add comprehensive tests for graph-upload module
by RajdeepKushwaha5 · 2026-02-04
79.5%
#23598: fix(msteams): add SSRF protection to attachment downloads via redir...
by lewiswigmore · 2026-02-22
79.1%
#21739: feat(msteams): support resumable upload sessions for files > 4MB
by krishna-warfade · 2026-02-20
79.0%
#23226: fix(msteams): proactive messaging, EADDRINUSE fix, tool status, ada...
by TarogStar · 2026-02-22
78.4%
#22884: feat(msteams): add resumable upload sessions with retry and range-a...
by chansuke · 2026-02-21
78.3%
#21440: fix(msteams): enforce allowlist checks on redirect hops (SSRF) (#11...
by Asm3r96 · 2026-02-19
77.9%
#23629: fix(msteams): sanitize error messages sent to users (CWE-209)
by lewiswigmore · 2026-02-22
77.3%