Skip to content

fix(auth): support qBittorrent 5.2+ login and session cookie - #180

Merged
jordanlambrecht merged 4 commits into
developmentfrom
fix/qbt-5.2-auth
Aug 13, 2026
Merged

fix(auth): support qBittorrent 5.2+ login and session cookie#180
jordanlambrecht merged 4 commits into
developmentfrom
fix/qbt-5.2-auth

Conversation

@jordanlambrecht

Copy link
Copy Markdown
Owner

Fixes qBittorrent client connections against qBittorrent 5.2.0+ (released 2026-05-03), which are currently broken for every user on that version or newer.

What broke

qBittorrent 5.2.0 changed two things about /api/v2/auth/login at once, and we depend on both:

  1. Login success is now 204 No Content with an empty body. loginAction() no longer calls setResult(), and webapplication.cpp maps a null result to 204. We required the body to equal "Ok.", so login threw "Authentication failed — check username and password" on correct credentials.
  2. The session cookie was renamed from SID to QBT_SID_<port>:
    const QString SESSION_COOKIE_NAME_PREFIX = u"QBT_SID_"_s;
    m_sessionCookieName = SESSION_COOKIE_NAME_PREFIX + QString::number(pref->getWebUIPort());
    We matched /SID=([^;]+)/, which doesn't match QBT_SID_8080=..., and we sent the value back as Cookie: SID=..., a name the server's exact-name lookup ignores. Either break alone is fatal.

Bad credentials also now return 401 instead of 200 with body "Fails.".

What this changes

  • Accept 204 as login success alongside the legacy 200 + "Ok.".
  • Model the session cookie as SidCookie { name, value } and thread it through getSession, withSessionRetry, qbtFetch, getTorrents, getTransferInfo, and syncMaindata. The cookie name is server-determined, so it can't stay a hardcoded constant.
  • Read the name off Set-Cookie rather than deriving it. The port in the name is qBittorrent's own configured WebUI port, not the port we connected on — behind a reverse proxy on :443 it's still the internal port, so deriving it from our config would be wrong.
  • Match ^QBT_SID_\d+$ or the legacy exact SID, keeping ≤5.1 servers working.
  • Parse each Set-Cookie header individually via getSetCookie(). .get("set-cookie") comma-joins multiple headers, and cookie values and Expires dates legally contain commas, so regexing across the joined string was unsafe. This fixes a latent bug that predates 5.2.

Net production change: 38 insertions, 15 deletions in src/lib/download-clients/qbt/transport.ts.

Verification

Behavior confirmed against qBittorrent source at the release-5.2.0 tag (src/webui/webapplication.cpp, src/webui/api/authcontroller.cpp). Note the GitHub wiki and most third-party docs still describe the 5.0 API and show plain SID= — they are stale on this point.

Local run against this branch:

tsc               exit=0
biome             exit=0    556 files, no issues
security-audit    exit=0    0 critical, 0 warning, 38/38 (matches baseline)
vitest            exit=0    103 files, 2794 tests (+6)

New tests cover 204 login, the QBT_SID_<port> name, a decoy cookie whose name merely contains SID as a substring, and the comma-joined-header bleed case.

Provenance

These four commits are cherry-picked from #175, authored by @clawdbrunner, and are unrelated to that PR's tracker-adapter work — they touch only src/lib/download-clients/. Split out so the client fix can ship without waiting on that review. When #175 rebases, git will recognize these as already applied and drop them automatically.

Not included

  • Bad credentials on 5.2+ still surface as qBittorrent API error: 401 Unauthorized rather than the friendlier credentials message, because the generic !response.ok check runs before the body check. Worth a follow-up.
  • API-key auth (Authorization: Bearer, qBittorrent 5.2.0+ / WebAPI 2.15.1) would remove cookie handling entirely, but auth/* is forbidden under a key so testConnection() would need to probe transfer/info instead. Separate change.

clawdbrunner and others added 4 commits August 13, 2026 15:00
qBittorrent 5.2.3RC returns 204 with an empty body on successful
/api/v2/auth/login instead of the pre-5.2 200 "Ok." body, which was
incorrectly treated as a failed login and surfaced as a generic
"Connection failed" error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qBittorrent 5.2+ names its session cookie QBT_SID_<port> (its own
WebUI listen port baked into the name) instead of the legacy plain
SID, so the /SID=([^;]+)/ regex in login() never matched and auth
kept failing even after the 204-status fix. Widen it to match any
cookie whose name contains SID as a token.

Verified live against a 5.2+ instance that the server accepts the
session value back under any cookie name (only the value is
validated, not the name), so qbtFetch's Cookie: SID=${sid} header
needs no change.
… SID

qBittorrent 5.2+ rejects the session value when sent back under the wrong
cookie name (verified live: same SID value, QBT_SID_8080=<value> -> 200,
SID=<value> -> 403). login() now returns {name, value} captured from the
Set-Cookie regex match instead of just the value, and that pair is plumbed
through sidCache/getSession/withSessionRetry/qbtFetch so the Cookie header
uses the actual assigned name.
The regex-over-comma-joined-header approach could pick the wrong cookie
(any name merely containing "SID", e.g. "SIDCC") and could bleed a cookie's
value across a comma boundary into the next Set-Cookie entry when the SID
cookie had no trailing attributes. getSetCookie() returns each Set-Cookie
header as its own array element, so each is parsed as a single cookie and
matched against an exact SID/QBT_SID_<port> name pattern.
@github-actions

Copy link
Copy Markdown

Knip Code Analysis

Found 8 total issues

Category Count
Unused Dependencies 1
Unused Dev Dependencies 3
Unused Exports 4
View details

Run pnpm knip locally to see the full report.

Use pnpm knip:filter pattern to filter results by file path.


Use /** @public */ JSDoc tags to mark intentionally exported symbols.

@github-actions

Copy link
Copy Markdown

✅ Security audit passed

Passed (38/38)

  • ✅ Auth enforcement on protected routes (per-handler)
  • ✅ No dangerous functions (eval, innerHTML, etc.)
  • ✅ No hardcoded secrets in source
  • ✅ Security headers in next.config.ts
  • ✅ Cookie security (httpOnly, sameSite, secure)
  • ✅ No sensitive fields in API responses
  • ✅ No .env files committed to repo
  • ✅ No raw SQL in API routes
  • ✅ No fetch/redirect with unvalidated URLs in routes
  • ✅ Timing-safe comparison for secret values
  • ✅ No raw SQL migration files (schema-first only)
  • ✅ External fetch calls have timeouts
  • ✅ Docker container runs as non-root user
  • ✅ Public routes match proxy allowlist
  • ✅ File delete operations have path traversal defense
  • ✅ Password hashing uses Argon2 (not SHA-256/bcrypt)
  • ✅ Encrypted columns written via encrypt()
  • ✅ TOTP 2FA flow integrity
  • ✅ Emergency lockdown flow integrity
  • ✅ Scrub & delete (nuke) flow integrity
  • ✅ Backup restore flow integrity
  • ✅ Login flow integrity
  • ✅ Auth result checked before proceeding
  • ✅ Backup password inputs bounded before key derivation
  • ✅ Webhook delivery fetch uses redirect: "error"
  • ✅ SESSION_SECRET minimum-length guard in auth/crypto modules
  • ✅ Notification URL validators include SSRF protection
  • ✅ Dockerfile does not COPY sensitive files
  • ✅ No secret env vars in client components
  • ✅ Adapter Cookie headers guard against injection
  • ✅ Adapter files do not log credential values
  • ✅ No console.log in API routes
  • ✅ No TODO/FIXME in security-critical files
  • ✅ JSON.parse wrapped in try-catch
  • ✅ No swallowed errors in catch blocks
  • ✅ Request body size validation on upload routes
  • ✅ BigInt fields use string serialization
  • ✅ No raw error messages in API responses

Summary: 38/38 checks passed

See scripts/security-audit.ts for check definitions and SECURITY.md for the full security architecture.

@jordanlambrecht
jordanlambrecht merged commit 6a79be1 into development Aug 13, 2026
5 of 6 checks passed
@jordanlambrecht
jordanlambrecht deleted the fix/qbt-5.2-auth branch August 13, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants