Version in base suite: 4.5.0+dfsg-1 Base version: sabnzbdplus_4.5.0+dfsg-1 Target version: sabnzbdplus_4.5.0+dfsg-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/contrib/s/sabnzbdplus/sabnzbdplus_4.5.0+dfsg-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/contrib/s/sabnzbdplus/sabnzbdplus_4.5.0+dfsg-1+deb13u1.dsc .gitlab-ci.yml | 22 ---- changelog | 7 + patches/11_security_fix_authentication_bypass.diff | 94 +++++++++++++++++++++ patches/series | 1 4 files changed, 103 insertions(+), 21 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmppjs46cp0/sabnzbdplus_4.5.0+dfsg-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmppjs46cp0/sabnzbdplus_4.5.0+dfsg-1+deb13u1.dsc: no acceptable signature found diff -Nru sabnzbdplus-4.5.0+dfsg/debian/.gitlab-ci.yml sabnzbdplus-4.5.0+dfsg/debian/.gitlab-ci.yml --- sabnzbdplus-4.5.0+dfsg/debian/.gitlab-ci.yml 2025-03-31 12:49:54.000000000 +0000 +++ sabnzbdplus-4.5.0+dfsg/debian/.gitlab-ci.yml 2026-08-13 07:53:01.000000000 +0000 @@ -3,28 +3,8 @@ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml variables: - RELEASE: 'unstable' + RELEASE: 'trixie' SALSA_CI_DISABLE_APTLY: 0 SALSA_CI_COMPONENTS: 'main contrib non-free' # no compiled code, no point running blhc SALSA_CI_DISABLE_BLHC: 'true' - -autopkgtest: - extends: .test-autopkgtest - variables: - SALSA_CI_AUTOPKGTEST_ARGS: '--setup-commands=debian/salsa/add-repositories.sh' - -piuparts: - # br0ken by https://salsa.debian.org/salsa-ci-team/pipeline/-/issues/312 ? - extends: .test-piuparts - allow_failure: true - variables: - SALSA_CI_PIUPARTS_PRE_INSTALL_SCRIPT: 'debian/salsa/add-repositories.sh' - -aptly: - extends: .publish-aptly - before_script: - - debian/salsa/add-repositories.sh - - debian/salsa/get-packages.sh "${WORKING_DIR}" - variables: - GIT_STRATEGY: fetch diff -Nru sabnzbdplus-4.5.0+dfsg/debian/changelog sabnzbdplus-4.5.0+dfsg/debian/changelog --- sabnzbdplus-4.5.0+dfsg/debian/changelog 2025-04-02 06:59:24.000000000 +0000 +++ sabnzbdplus-4.5.0+dfsg/debian/changelog 2026-08-13 07:53:01.000000000 +0000 @@ -1,3 +1,10 @@ +sabnzbdplus (4.5.0+dfsg-1+deb13u1) trixie-security; urgency=high + + * Patches: add 11, backport of an upstream security fix for an + authentication bypass in the web interface. + + -- Jeroen Ploemen Thu, 13 Aug 2026 07:53:01 +0000 + sabnzbdplus (4.5.0+dfsg-1) unstable; urgency=medium * New upstream release. diff -Nru sabnzbdplus-4.5.0+dfsg/debian/patches/11_security_fix_authentication_bypass.diff sabnzbdplus-4.5.0+dfsg/debian/patches/11_security_fix_authentication_bypass.diff --- sabnzbdplus-4.5.0+dfsg/debian/patches/11_security_fix_authentication_bypass.diff 1970-01-01 00:00:00.000000000 +0000 +++ sabnzbdplus-4.5.0+dfsg/debian/patches/11_security_fix_authentication_bypass.diff 2026-08-13 07:53:01.000000000 +0000 @@ -0,0 +1,94 @@ +--- a/sabnzbd/interface.py ++++ b/sabnzbd/interface.py +@@ -31,6 +31,7 @@ + import ssl + import functools + import copy ++import secrets + from random import randint + from xml.sax.saxutils import escape + from Cheetah.Template import Template +@@ -261,7 +262,7 @@ + + + # Create a more unique ID for each instance +-COOKIE_SECRET = str(randint(1000, 100000) * os.getpid()) ++COOKIE_SECRET = secrets.token_hex(32) + + + def remote_ip_from_xff(xff_ips: List[str]) -> str: +@@ -293,27 +294,29 @@ + else: + remote_ip = cherrypy.request.remote.ip + +- cookie_str = utob(str(salt) + remote_ip + COOKIE_SECRET) +- cherrypy.response.cookie["login_cookie"] = hashlib.sha1(cookie_str).hexdigest() +- cherrypy.response.cookie["login_cookie"]["path"] = "/" +- cherrypy.response.cookie["login_cookie"]["httponly"] = 1 +- cherrypy.response.cookie["login_salt"] = salt +- cherrypy.response.cookie["login_salt"]["path"] = "/" +- cherrypy.response.cookie["login_salt"]["httponly"] = 1 +- +- # If we want to be remembered +- if remember_me: +- cherrypy.response.cookie["login_cookie"]["max-age"] = 3600 * 24 * 14 +- cherrypy.response.cookie["login_salt"]["max-age"] = 3600 * 24 * 14 +- + # To remove + if remove: ++ # Never emit a valid cookie/salt pair on logout ++ cherrypy.response.cookie["login_cookie"] = "" ++ cherrypy.response.cookie["login_salt"] = "" + cherrypy.response.cookie["login_cookie"]["expires"] = 0 + cherrypy.response.cookie["login_salt"]["expires"] = 0 + else: ++ cookie_str = utob(str(salt) + remote_ip + COOKIE_SECRET) ++ cherrypy.response.cookie["login_cookie"] = hashlib.sha1(cookie_str).hexdigest() ++ cherrypy.response.cookie["login_salt"] = salt ++ # If we want to be remembered ++ if remember_me: ++ cherrypy.response.cookie["login_cookie"]["max-age"] = 3600 * 24 * 14 ++ cherrypy.response.cookie["login_salt"]["max-age"] = 3600 * 24 * 14 + # Notify about new login + notifier.send_notification(T("User logged in"), T("User logged in to the web interface"), "new_login") + ++ cherrypy.response.cookie["login_cookie"]["path"] = "/" ++ cherrypy.response.cookie["login_cookie"]["httponly"] = 1 ++ cherrypy.response.cookie["login_salt"]["path"] = "/" ++ cherrypy.response.cookie["login_salt"]["httponly"] = 1 ++ + + def check_login_cookie(): + # Do we have everything? +--- a/tests/test_interface.py ++++ b/tests/test_interface.py +@@ -224,3 +224,29 @@ + assert interface.remote_ip_from_xff(xff_ips) is expected_result + + _func() ++ ++ @set_config({"verify_xff_header": False}) ++ def test_logout_does_not_leak_valid_cookie(self): ++ """A logout must never emit a cookie/salt pair that passes check_login_cookie. ++ The Set-Cookie header is readable regardless of its expiry, so leaking valid ++ values there is an authentication bypass (harvest via logout, then replay).""" ++ cherrypy.request.remote.ip = "10.11.12.13" ++ cherrypy.request.headers.update({"X-Forwarded-For": None}) ++ ++ # Sanity check: a real login produces a cookie that validates ++ cherrypy.response.cookie.clear() ++ interface.set_login_cookie() ++ cherrypy.request.cookie["login_cookie"] = cherrypy.response.cookie["login_cookie"].value ++ cherrypy.request.cookie["login_salt"] = cherrypy.response.cookie["login_salt"].value ++ assert interface.check_login_cookie() is True ++ ++ # Logout must blank out the values, not emit a working hash/salt ++ cherrypy.response.cookie.clear() ++ interface.set_login_cookie(remove=True) ++ assert cherrypy.response.cookie["login_cookie"].value == "" ++ assert cherrypy.response.cookie["login_salt"].value == "" ++ ++ # Replaying whatever the logout response carried must fail authentication ++ cherrypy.request.cookie["login_cookie"] = cherrypy.response.cookie["login_cookie"].value ++ cherrypy.request.cookie["login_salt"] = cherrypy.response.cookie["login_salt"].value ++ assert interface.check_login_cookie() is False diff -Nru sabnzbdplus-4.5.0+dfsg/debian/patches/series sabnzbdplus-4.5.0+dfsg/debian/patches/series --- sabnzbdplus-4.5.0+dfsg/debian/patches/series 2025-03-31 12:49:54.000000000 +0000 +++ sabnzbdplus-4.5.0+dfsg/debian/patches/series 2026-08-13 07:53:01.000000000 +0000 @@ -3,3 +3,4 @@ 08_disable_new_version_check.diff 09_remove_external_resources.diff 10_pytest_mods.diff +11_security_fix_authentication_bypass.diff