Add browser-like headers to all Slack-bound traffic (refs #5)#22
Add browser-like headers to all Slack-bound traffic (refs #5)#22gammons wants to merge 9 commits into
Conversation
Adds a Slack-host-aware http.RoundTripper that decorates outbound requests to *.slack.com with Chrome-like User-Agent, Origin, Referer, Accept, Accept-Language, and Sec-Fetch-* headers. Caller-set headers are preserved. Non-Slack hosts pass through unchanged. Wiring follows in subsequent commits. Refs #5
Code review caught that BrowserTransport.RoundTrip panics when a caller constructs a literal *http.Request with Header == nil (since http.Header.Clone() returns nil on a nil receiver and the subsequent setIfMissing writes hit a nil map). Defend against both nil Header and nil URL — neither fires from slk's current call sites, but the slackhttp package is public-shaped and shouldn't have latent panics on legal-but-unusual request shapes. Also rename the test-local 'cap' variable (which shadowed Go's builtin) to 'recorder' and tighten http.NewRequest error handling in the test file. Refs #5
NewClient and newCookieHTTPClient now construct the inner http.Client via slackhttp.NewBrowserHTTPClient, so every slack-go Web API call AND every hand-rolled endpoint POST (which all share the same client) is decorated with browser-like User-Agent, Origin, Referer, Accept, Accept-Language, and Sec-Fetch-* headers. Refs #5
StartWebSocket now passes the same User-Agent, Accept-Language, and Sec-Fetch-* headers BrowserTransport adds to HTTP calls. Sec-Fetch-Dest is narrowed to 'websocket' to match what a real browser sends. Refs #5
The image fetcher previously announced itself with a unique User-Agent (slk/inline-image-fetcher). Replace it with a default http.Client wrapped in slackhttp.BrowserTransport so file-CDN requests on *.slack.com carry the same browser-like headers the rest of slk's traffic does. Per-request Authorization Bearer + 'd' cookie remain unchanged. Refs #5
Records what's been verified automatically (tests, vet, build, header absence checks) vs what still needs the maintainer's eyes (mitmproxy/wire-level inspection against a real workspace, functional smoke test, Enterprise Grid retest). The draft issue #5 comment is intentionally not auto-posted — it should ship only after the maintainer has confirmed wire-level verification. Refs #5
The plan that drove this branch. Captured here for historical context alongside the verification-notes companion already committed. Refs #5
Slack's official browser client never sends Authorization: Bearer to app.slack.com — it puts the token in the form body as token=xoxc-... . slk's five hand-rolled endpoints (client.counts, conversations.mark, subscriptions.thread.mark, users.prefs.get, users.channelSections.list, subscriptions.thread.getView) were using the Bearer pattern. Once we started sending browser-shaped headers (Origin: app.slack.com, Mozilla UA, Sec-Fetch-*) in the previous commits, the combination of browser headers + Bearer auth on the same request became a contradictory signature: real browsers never do that. An Enterprise Grid user who retested PR #22 reported that login and channel-list (slack-go-driven calls that already used form-body tokens) worked, then selecting a channel — which fires these hand-rolled endpoints in rapid succession — logged them out of all clients. This commit aligns the hand-rolled endpoints with slack-go's convention: token in the form body, no Authorization header. The image fetcher's Authorization: Bearer for files.slack.com downloads is a different surface (CDN auth) and is unchanged for now — the user's report was channel-select-specific, not image-load. Refs #5
|
Update — fix pushed in response to user retest feedback @focusaurus / @raff / @antoszka: an Enterprise Grid user retested the Bearer is an OAuth pattern. Real browsers never send Bearer to Commit 7aaf302 Empirical evidence the new shape works on Enterprise: it's exactly what If you're up for another round of testing on Enterprise Grid, this fix |
Summary
Addresses #5. Decorates every outbound request to
*.slack.comwith theHTTP headers a recent desktop Chrome on
app.slack.comwould send, soslk's
xoxc-token traffic looks indistinguishable from the officialbrowser client at the header level. Goal: stop Enterprise Grid anomaly
detectors from signing users out / triggering security alerts.
What changed
internal/slackhttppackage —BrowserTransport(
http.RoundTripper) that addsUser-Agent(per-OS Chrome 120),Accept,Accept-Language,Origin,Referer, andSec-Fetch-*headers to requests bound for
*.slack.comhosts. Never overridescaller-set headers (so the per-team
Authorization: Bearer xoxc-...Cookie: d=...on the image fetcher and the hand-rolled endpointssurvive). Non-Slack hosts pass through untouched.
internal/slack/client.go) now constructsits inner
*http.Clientviaslackhttp.NewBrowserHTTPClient, soevery Web API call AND every hand-rolled endpoint POST inherits the
headers for free.
wsUpgradeHeaders()helper, withSec-Fetch-Dest: websocketto matchwhat a real browser sends on a WS open. (Gorilla doesn't go through
http.RoundTripper, so the headers are passed inline toDialer.Dial.)slk/inline-image-fetcherUser-Agent (agiveaway just as flagable as
Go-http-client/1.1) and now goesthrough
BrowserTransportvia the client constructed inmain.go.best-effort mitigation, please file an issue if you still get booted.
Out of scope (intentionally)
doesn't move the needle.
to undocumented endpoints like
client.counts,users.channelSections.list, etc.) and require enterprise adminapproval. Tracked separately.
needs it.
Test plan
Automated (all passing in CI / locally):
go test ./... -race -count=1— all 39 packages passgo vet ./...cleango build ./...cleanBrowserTransportcover positive (Slack-hostrequests get all 8 headers), negative (non-Slack hosts untouched),
subdomain matrix (
slack.com,files.slack.com,hackclub.enterprise.slack.com,wss-primary.slack.com),caller-header preservation (UA / Authorization / Cookie), nil
Header/URL defense, per-GOOS UA selection
transport is
*slackhttp.BrowserTransport; WS upgrade carriesUser-Agent: Mozilla/...,Origin: https://app.slack.com,Accept-Language, andSec-Fetch-Dest: websocket; image fetcherno longer emits the legacy
slk/inline-image-fetcherUAslk/inline-image-fetcherorGo-http-clientin production codeManual (maintainer eyes required before broad announce — see
docs/superpowers/plans/2026-05-20-browser-like-headers-verification-notes.md):front; visually confirm every Slack request carries the expected
headers and that neither
Go-http-clientnorslk/inline-image-fetcherappearsimage attachment → thread reply
@focusaurus / @raff / @antoszka to retest on Enterprise Grid
Important caveats
This may not be sufficient. The next escalation is TLS fingerprinting
(Go's ClientHello looks nothing like Chrome's), which would require
uTLS. We won't know whether header parity is enough until an Enterprise
Grid user retests. The README and the draft #5 comment frame this
honestly.