Skip to content

[BUG] Slack attachment download has no host allowlist (Discord's downloader does) #4771

Description

@larsspinetta12

Summary

gateway/slack/attachments.py downloads Slack files with follow_redirects=True
and no host allowlist, so the bot token is sent to whatever host url_private
names. Discord's downloader (gateway/discord/attachments.py) rejects non-Discord
hosts before the request. Slack should have the same check.

Scope correction from the original report: the bot token does not leak across
a redirect. httpx strips the Authorization header whenever a redirect leaves the
origin (_client.py::_redirect_headers, present in the pinned httpx>=0.27); the
only exception is a same-host http→https upgrade, and url_private is already
https. The original "observe the Bearer header is re-sent to the redirect target"
step does not reproduce, and the original "Done when" was already satisfied by the
library. What is missing is the host check itself.

This is defense in depth, not a live exploit — see the threat model below.

Expected vs actual behavior

Expected: reject a url_private whose host is not a known Slack file host
before sending the bearer token, and re-check the host on every redirect hop.

Actual: download_file (L106–114) sends Authorization: Bearer {token} to
whatever host url_private names, with follow_redirects=True and no allowlist.

Steps to reproduce

  1. Build a SlackInboundFile whose url_private points at a non-Slack host.
  2. Call download_file(file, token).
  3. The request goes out. Nothing rejects the host.

There is no user-facing repro — the threat model below explains why.

Can you reproduce it consistently?

Yes

How often does it occur?

Under specific conditions (only reachable if url_private is ever attacker-influenced,
which it is not today)

Operating system

Other (server/gateway)

Additional context

Threat model — why this is hardening, not an incident.

The Slack gateway runs over Socket Mode (gateway/slack/socket_mode_worker.py).
There is no HTTP Slack events route in gateway/http/, so url_private arrives
only over Slack's authenticated WebSocket and is minted by Slack, not by the
message sender. An external attacker cannot set it as the code stands.

The check is worth adding anyway because it guards the invariant if an HTTP Events
API route is added later, if the files.info fallback noted in
gateway/slack/events.py L118 starts supplying the URL, or if httpx changes its
redirect handling. It also closes a second-order gap that stripping Authorization
does not: a redirect chain currently lets the gateway issue outbound GETs to
arbitrary hosts, and the response body is decoded and inlined into the model turn
(extract_text_budgeted_section).

Implementation note — do not just copy Discord.

Discord sets follow_redirects=False. Slack cannot: url_private legitimately
307/308-redirects to the file CDN, so disabling redirects breaks every download.
The Slack fix needs a manual hop loop — follow_redirects=False, then re-request
the Location only when its host passes the allowlist, bounded to a few hops.

  • Add SLACK_ATTACHMENT_HOST_SUFFIXES to config/constants/slack.py, mirroring
    DISCORD_ATTACHMENT_HOST_SUFFIXES in config/constants/discord.py.
  • Reuse the shape of is_allowed_attachment_url (https-only, exact host or
    dot-suffix match).
  • Confirm the current documented Slack file hosts before pinning the tuple;
    files.slack.com is the one in use today.
  • Keep the existing 10s timeout and the 256 KiB _MAX_FILE_BYTES cap.

Tests:

  • non-allowlisted initial host → no request issued, None returned
  • allowlisted host that redirects to a non-allowlisted host → chain stops, None
  • allowlisted host redirecting within the allowlist → bytes returned, cap still enforced
  • hop limit → a redirect loop terminates

Done when: download_file will not send the bot token to a host outside the
Slack file-host allowlist, on the initial request or on any redirect hop.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingmessagingsecuritySecurity hardening and sensitive-data safety

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions