Skip to content

Commit 6613b98

Browse files
committed
fix(auth): send Origin+Referer + full cookie set to defeat 403 on UserByRestId
Live test on macOS hit a hard 403 from UserByRestId even with a valid session pinned via --profile. Two missing pieces vs what a real Chrome sends, both fixed: 1. Origin and Referer headers were never set. x-cli already declared `sec-fetch-site: same-origin` but didn't send the actual Origin header. X's gateway uses Origin as its same-origin CSRF check on top of x-csrf-token, and rejects the request when it can't validate where it came from. Real browsers add Origin/Referer automatically; Go's net/http does not. api/client.go applyHeaders now sets: Origin: https://x.com Referer: https://x.com/ on every request. Authenticated and unauthenticated alike — every request x-cli makes is to x.com, so this is unconditionally correct. 2. We were filtering the imported cookie set down to 6 names. The browser-import path used a cookieNamesWanted allowlist (auth_token, ct0, twid, kdt, att, guest_id). Real browsers send the ENTIRE cookie set on every request — typically 25-50 cookies for x.com. Dropping personalization_id, gt, _twitter_sess, lang, etc. lets X's anti-abuse model fingerprint us as "non-browser" because no real Chrome would ever omit them. cmd/auth.go now imports every cookie kooky returns for x.com (no filter), and only enforces that auth_token + ct0 are present (the cookieNamesRequired set, renamed from Wanted). Verbose output prints the imported cookie names (not values) so the user can see what we have to work with. summarizeCookieNames + a tiny sortStrings helper for that. Net result: same hosting overhead, ~1KB more in the keychain blob, but the wire matches what Chrome sends and the same-origin check should now pass. If the 403 persists after this, the next layer is TLS fingerprinting (JA3/JA4 from Go stdlib != Chrome) which needs `refraction-networking /utls`. That was on the v0.1 plan; this commit does not yet wire it.
1 parent 39091ff commit 6613b98

2 files changed

Lines changed: 38 additions & 18 deletions

File tree

api/client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ func (c *Client) applyHeaders(req *http.Request, opts requestOpts) {
213213
req.Header.Set("x-twitter-active-user", "yes")
214214
req.Header.Set("x-twitter-client-language", "en")
215215

216+
// Origin + Referer are required for X's same-origin CSRF check.
217+
// Without them the gateway treats the call as cross-site and 403s
218+
// authenticated reads. Real browsers add these automatically; Go's
219+
// net/http does not, so we set them ourselves.
220+
req.Header.Set("Origin", "https://x.com")
221+
req.Header.Set("Referer", "https://x.com/")
222+
216223
// Client-hint headers matched to the UA. Pinned, not rotated per-request.
217224
req.Header.Set("sec-ch-ua", `"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"`)
218225
req.Header.Set("sec-ch-ua-mobile", "?0")

cmd/auth.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,12 @@ func runAuthBrowsers(cmd *cobra.Command, _ []string) error {
125125
return nil
126126
}
127127

128-
// cookieNamesWanted is the subset of browser cookies x-cli imports.
129-
// auth_token and ct0 are required; twid carries the user id so we can
130-
// skip a UserByScreenName roundtrip on `auth status`; the others are
131-
// helpful for stable header building but not strictly required.
132-
var cookieNamesWanted = []string{"auth_token", "ct0", "twid", "kdt", "att", "guest_id"}
128+
// cookieNamesRequired must be present after import or x-cli refuses
129+
// to save the session. Everything else kooky reads from the browser
130+
// store is also imported — real browsers send the full cookie set on
131+
// every request, and dropping minor cookies (personalization_id, gt,
132+
// _twitter_sess) confuses X's same-origin CSRF check on some routes.
133+
var cookieNamesRequired = []string{"auth_token", "ct0"}
133134

134135
func runAuthImport(cmd *cobra.Command, _ []string) error {
135136
cmdutil.Warn("Reminder: x-cli uses your real logged-in session. Automation can get")
@@ -258,12 +259,17 @@ func readBrowserCookies(browser, profile string, softMiss bool) (string, error)
258259
return "", err
259260
}
260261

261-
raw := browsercookies.FormatCookieHeader(res.Cookies, cookieNamesWanted)
262-
if raw == "" {
263-
return "", fmt.Errorf("required cookies (auth_token, ct0) not in %s/%s — are you logged in to x.com on that profile?", res.Browser, res.Profile)
262+
// Send everything the browser stores for x.com. Filtering down to
263+
// a "wanted" subset breaks X's same-origin checks on some routes.
264+
raw := browsercookies.FormatCookieHeader(res.Cookies, nil)
265+
for _, name := range cookieNamesRequired {
266+
if v, ok := res.Cookies[name]; !ok || v == "" {
267+
return "", fmt.Errorf("required cookie %q not present in %s/%s — are you logged in to x.com on that profile?", name, res.Browser, res.Profile)
268+
}
264269
}
265270

266271
cmdutil.Success("using %s / %s (%s)", res.Browser, res.Profile, res.Source)
272+
cmdutil.Info("imported %d cookie(s): %s", len(res.Cookies), summarizeCookieNames(res.Cookies))
267273
if len(res.Alternatives) > 0 {
268274
cmdutil.Warn("also found x.com sessions in:")
269275
for _, a := range res.Alternatives {
@@ -274,20 +280,27 @@ func readBrowserCookies(browser, profile string, softMiss bool) (string, error)
274280
return raw, nil
275281
}
276282

277-
func promptCookiePaste() (string, error) {
278-
return cmdutil.ReadSecret("Paste cookie header (auth_token=...; ct0=...): ")
283+
// summarizeCookieNames returns a comma-separated, sorted list of cookie
284+
// names — values are never logged.
285+
func summarizeCookieNames(cookies map[string]string) string {
286+
names := make([]string, 0, len(cookies))
287+
for k := range cookies {
288+
names = append(names, k)
289+
}
290+
sortStrings(names)
291+
return strings.Join(names, ", ")
279292
}
280293

281-
// countCookies returns how many of the wanted names are present in the
282-
// cookie map. Used only for the friendly log line.
283-
func countCookies(cookies map[string]string, wanted []string) int {
284-
n := 0
285-
for _, k := range wanted {
286-
if v, ok := cookies[k]; ok && v != "" {
287-
n++
294+
func sortStrings(s []string) {
295+
for i := 1; i < len(s); i++ {
296+
for j := i; j > 0 && s[j] < s[j-1]; j-- {
297+
s[j], s[j-1] = s[j-1], s[j]
288298
}
289299
}
290-
return n
300+
}
301+
302+
func promptCookiePaste() (string, error) {
303+
return cmdutil.ReadSecret("Paste cookie header (auth_token=...; ct0=...): ")
291304
}
292305

293306
func runAuthStatus(cmd *cobra.Command, _ []string) error {

0 commit comments

Comments
 (0)