[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1108869: marked as done (unblock: qbittorrent/5.1.0-2)



Your message dated Thu, 10 Jul 2025 12:24:25 +0000
with message-id <E1uZqK9-00EGDV-2G@respighi.debian.org>
and subject line unblock qbittorrent
has caused the Debian Bug report #1108869,
regarding unblock: qbittorrent/5.1.0-2
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
1108869: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1108869
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
X-Debbugs-Cc: qbittorrent@packages.debian.org
Control: affects -1 + src:qbittorrent
User: release.debian.org@packages.debian.org
Usertags: unblock

Please unblock package qbittorrent

Package not yet uploaded.

To fix bug #1108843 with security issue (no DSA yet)

I've two choices: backport 2 patches from 5.1.2 or release the 5.1.2
version over 5.1.0

debdiff at the bottom.

What is your opinion?

Christian

$ cat qbittorrent_5.1.0-2.debdiff
diff -Nru qbittorrent-5.1.0/debian/changelog qbittorrent-5.1.0/debian/changelog
--- qbittorrent-5.1.0/debian/changelog	2025-04-28 09:24:06.000000000 +0200
+++ qbittorrent-5.1.0/debian/changelog	2025-07-06 16:40:13.000000000 +0200
@@ -1,3 +1,10 @@
+qbittorrent (5.1.0-2) unstable; urgency=medium
+
+  * Add two patches from 5.1.2 version to fix security issues: WebAPI, Rss
+    and Search modules (Closes: #1108843)
+
+ -- Christian Marillat <marillat@debian.org>  Sun, 06 Jul 2025 16:40:13 +0200
+
 qbittorrent (5.1.0-1) unstable; urgency=medium
 
   * New upstream release.
diff -Nru qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch
--- qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch	1970-01-01 01:00:00.000000000 +0100
+++ qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch	2025-07-06 16:39:40.000000000 +0200
@@ -0,0 +1,191 @@
+From d379fa30350bd2aaf50656c7cd5fbaf6f6219773 Mon Sep 17 00:00:00 2001
+From: "Vladimir Golovnev (Glassez)" <glassez@yandex.ru>
+Date: Mon, 23 Jun 2025 13:14:37 +0300
+Subject: [PATCH 1/2] Prevent opening local files if web page is expected
+
+---
+ src/base/rss/rss_article.cpp       | 20 ++++++++++----------
+ src/gui/rss/rsswidget.cpp          |  4 ++--
+ src/gui/search/searchjobwidget.cpp |  8 ++++----
+ 3 files changed, 16 insertions(+), 16 deletions(-)
+
+--- a/src/base/rss/rss_article.cpp
++++ b/src/base/rss/rss_article.cpp
+@@ -48,16 +48,16 @@ const QString Article::KeyIsRead = u"isR
+ 
+ Article::Article(Feed *feed, const QVariantHash &varHash)
+     : QObject(feed)
+-    , m_feed(feed)
+-    , m_guid(varHash.value(KeyId).toString())
+-    , m_date(varHash.value(KeyDate).toDateTime())
+-    , m_title(varHash.value(KeyTitle).toString())
+-    , m_author(varHash.value(KeyAuthor).toString())
+-    , m_description(varHash.value(KeyDescription).toString())
+-    , m_torrentURL(varHash.value(KeyTorrentURL).toString())
+-    , m_link(varHash.value(KeyLink).toString())
+-    , m_isRead(varHash.value(KeyIsRead, false).toBool())
+-    , m_data(varHash)
++    , m_feed {feed}
++    , m_guid {varHash.value(KeyId).toString()}
++    , m_date {varHash.value(KeyDate).toDateTime()}
++    , m_title {varHash.value(KeyTitle).toString()}
++    , m_author {varHash.value(KeyAuthor).toString()}
++    , m_description {varHash.value(KeyDescription).toString()}
++    , m_torrentURL {varHash.value(KeyTorrentURL).toString()}
++    , m_link {varHash.value(KeyLink).toString()}
++    , m_isRead {varHash.value(KeyIsRead, false).toBool()}
++    , m_data {varHash}
+ {
+ }
+ 
+--- a/src/gui/rss/rsswidget.cpp
++++ b/src/gui/rss/rsswidget.cpp
+@@ -40,6 +40,7 @@
+ #include <QString>
+ 
+ #include "base/global.h"
++#include "base/logger.h"
+ #include "base/net/downloadmanager.h"
+ #include "base/preferences.h"
+ #include "base/rss/rss_article.h"
+@@ -415,16 +416,52 @@ void RSSWidget::downloadSelectedTorrents
+ // open the url of the selected RSS articles in the Web browser
+ void RSSWidget::openSelectedArticlesUrls()
+ {
++    qsizetype emptyLinkCount = 0;
++    qsizetype badLinkCount = 0;
++    QString articleTitle;
+     for (QListWidgetItem *item : asConst(m_ui->articleListWidget->selectedItems()))
+     {
+         auto *article = item->data(Qt::UserRole).value<RSS::Article *>();
+         Q_ASSERT(article);
+ 
+-        // Mark as read
+         article->markAsRead();
+ 
+-        if (!article->link().isEmpty())
+-            QDesktopServices::openUrl(QUrl(article->link()));
++        const QString articleLink = article->link();
++        const QUrl articleLinkURL {articleLink};
++        if (articleLinkURL.isEmpty()) [[unlikely]]
++        {
++            if (articleTitle.isEmpty())
++                articleTitle = article->title();
++            ++emptyLinkCount;
++        }
++        else if (articleLinkURL.isLocalFile()) [[unlikely]]
++        {
++            if (badLinkCount == 0)
++                articleTitle = article->title();
++            ++badLinkCount;
++
++            LogMsg(tr("Blocked opening RSS article URL. URL pointing to local file might be malicious behaviour. Article: \"%1\". URL: \"%2\".")
++                    .arg(article->title(), articleLink), Log::WARNING);
++        }
++        else [[likely]]
++        {
++            QDesktopServices::openUrl(articleLinkURL);
++        }
++    }
++
++    if (badLinkCount > 0)
++    {
++        QString message = tr("Blocked opening RSS article URL. The following article URL is pointing to local file and it may be malicious behaviour:\n%1").arg(articleTitle);
++        if (badLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more articles with the same issue.").arg(badLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
++    }
++    else if (emptyLinkCount > 0)
++    {
++        QString message = tr("The following article has no news URL provided:\n%1").arg(articleTitle);
++        if (emptyLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more articles with the same issue.").arg(emptyLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
+     }
+ }
+ 
+--- a/src/gui/search/searchjobwidget.cpp
++++ b/src/gui/search/searchjobwidget.cpp
+@@ -35,10 +35,12 @@
+ #include <QHeaderView>
+ #include <QKeyEvent>
+ #include <QMenu>
++#include <QMessageBox>
+ #include <QPalette>
+ #include <QStandardItemModel>
+ #include <QUrl>
+ 
++#include "base/logger.h"
+ #include "base/preferences.h"
+ #include "base/search/searchdownloadhandler.h"
+ #include "base/search/searchhandler.h"
+@@ -319,15 +321,52 @@ void SearchJobWidget::downloadTorrents(c
+         downloadTorrent(rowIndex, option);
+ }
+ 
+-void SearchJobWidget::openTorrentPages() const
++void SearchJobWidget::openTorrentPages()
+ {
+-    const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()};
++    const QModelIndexList rows = m_ui->resultsBrowser->selectionModel()->selectedRows();
++    qsizetype emptyLinkCount = 0;
++    qsizetype badLinkCount = 0;
++    QString warningEntryName;
+     for (const QModelIndex &rowIndex : rows)
+     {
+-        const QString descrLink = m_proxyModel->data(
+-                    m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK)).toString();
+-        if (!descrLink.isEmpty())
+-            QDesktopServices::openUrl(QUrl::fromEncoded(descrLink.toUtf8()));
++        const QString entryName = m_proxyModel->index(rowIndex.row(), SearchSortModel::NAME).data().toString();
++        const QString descrLink = m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK).data().toString();
++
++        const QUrl descrLinkURL {descrLink};
++        if (descrLinkURL.isEmpty()) [[unlikely]]
++        {
++            if (warningEntryName.isEmpty())
++                warningEntryName = entryName;
++            ++emptyLinkCount;
++        }
++        else if (descrLinkURL.isLocalFile()) [[unlikely]]
++        {
++            if (badLinkCount == 0)
++                warningEntryName = entryName;
++            ++badLinkCount;
++
++            LogMsg(tr("Blocked opening search result description page URL. URL pointing to local file might be malicious behaviour. Name: \"%1\". URL: \"%2\".")
++                    .arg(entryName, descrLink), Log::WARNING);
++        }
++        else [[likely]]
++        {
++            QDesktopServices::openUrl(descrLinkURL);
++        }
++    }
++
++    if (badLinkCount > 0)
++    {
++        QString message = tr("Blocked opening search result description page URL. The following result URL is pointing to local file and it may be malicious behaviour:\n%1").arg(warningEntryName);
++        if (badLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more results with the same issue.").arg(badLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
++    }
++    else if (emptyLinkCount > 0)
++    {
++        QString message = tr("Entry \"%1\" has no description page URL provided.").arg(warningEntryName);
++        if (emptyLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more entries with the same issue.").arg(emptyLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
+     }
+ }
+ 
+--- a/src/gui/search/searchjobwidget.h
++++ b/src/gui/search/searchjobwidget.h
+@@ -127,7 +127,7 @@ private:
+     void onUIThemeChanged();
+ 
+     void downloadTorrents(AddTorrentOption option = AddTorrentOption::Default);
+-    void openTorrentPages() const;
++    void openTorrentPages();
+     void copyTorrentURLs() const;
+     void copyTorrentDownloadLinks() const;
+     void copyTorrentNames() const;
diff -Nru qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch
--- qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch	1970-01-01 01:00:00.000000000 +0100
+++ qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch	2025-07-06 16:39:44.000000000 +0200
@@ -0,0 +1,37 @@
+From 9b29d37d210f28f46eb4758611d7b06f4603b9d1 Mon Sep 17 00:00:00 2001
+From: Chocobo1 <Chocobo1@users.noreply.github.com>
+Date: Mon, 30 Jun 2025 01:39:03 +0800
+Subject: [PATCH] WebAPI: Trim leading whitespaces on Run External Program
+ fields
+
+Hacked qbt instances may contain malicious script placed in Run External Program and the script
+will attempt to hide itself by adding a lot whitespaces at the start of the command string.
+Users may mistake the field of being empty but is actually not.
+So trim the leading whitespaces to easily expose the malicious script.
+
+Note that GUI already trim the fields and only WebAPI doesn't trim them. This patch will unify
+the behavior.
+Related: https://github.com/qbittorrent/docker-qbittorrent-nox/issues/71#issuecomment-2993567440
+
+PR #22939.
+---
+ src/webui/api/appcontroller.cpp | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+--- a/src/webui/api/appcontroller.cpp
++++ b/src/webui/api/appcontroller.cpp
+@@ -673,12 +673,12 @@ void AppController::setPreferencesAction
+     if (hasKey(u"autorun_on_torrent_added_enabled"_s))
+         pref->setAutoRunOnTorrentAddedEnabled(it.value().toBool());
+     if (hasKey(u"autorun_on_torrent_added_program"_s))
+-        pref->setAutoRunOnTorrentAddedProgram(it.value().toString());
++        pref->setAutoRunOnTorrentAddedProgram(it.value().toString().trimmed());
+     // Run an external program on torrent finished
+     if (hasKey(u"autorun_enabled"_s))
+         pref->setAutoRunOnTorrentFinishedEnabled(it.value().toBool());
+     if (hasKey(u"autorun_program"_s))
+-        pref->setAutoRunOnTorrentFinishedProgram(it.value().toString());
++        pref->setAutoRunOnTorrentFinishedProgram(it.value().toString().trimmed());
+ 
+     // Connection
+     // Listening Port
diff -Nru qbittorrent-5.1.0/debian/patches/series qbittorrent-5.1.0/debian/patches/series
--- qbittorrent-5.1.0/debian/patches/series	1970-01-01 01:00:00.000000000 +0100
+++ qbittorrent-5.1.0/debian/patches/series	2025-07-06 16:39:32.000000000 +0200
@@ -0,0 +1,2 @@
+4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch
+9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch
diff -Nru qbittorrent-5.1.0/debian/changelog qbittorrent-5.1.0/debian/changelog
--- qbittorrent-5.1.0/debian/changelog	2025-04-28 09:24:06.000000000 +0200
+++ qbittorrent-5.1.0/debian/changelog	2025-07-06 16:40:13.000000000 +0200
@@ -1,3 +1,10 @@
+qbittorrent (5.1.0-2) unstable; urgency=medium
+
+  * Add two patches from 5.1.2 version to fix security issues: WebAPI, Rss
+    and Search modules (Closes: #1108843)
+
+ -- Christian Marillat <marillat@debian.org>  Sun, 06 Jul 2025 16:40:13 +0200
+
 qbittorrent (5.1.0-1) unstable; urgency=medium
 
   * New upstream release.
diff -Nru qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch
--- qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch	1970-01-01 01:00:00.000000000 +0100
+++ qbittorrent-5.1.0/debian/patches/4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch	2025-07-06 16:39:40.000000000 +0200
@@ -0,0 +1,191 @@
+From d379fa30350bd2aaf50656c7cd5fbaf6f6219773 Mon Sep 17 00:00:00 2001
+From: "Vladimir Golovnev (Glassez)" <glassez@yandex.ru>
+Date: Mon, 23 Jun 2025 13:14:37 +0300
+Subject: [PATCH 1/2] Prevent opening local files if web page is expected
+
+---
+ src/base/rss/rss_article.cpp       | 20 ++++++++++----------
+ src/gui/rss/rsswidget.cpp          |  4 ++--
+ src/gui/search/searchjobwidget.cpp |  8 ++++----
+ 3 files changed, 16 insertions(+), 16 deletions(-)
+
+--- a/src/base/rss/rss_article.cpp
++++ b/src/base/rss/rss_article.cpp
+@@ -48,16 +48,16 @@ const QString Article::KeyIsRead = u"isR
+ 
+ Article::Article(Feed *feed, const QVariantHash &varHash)
+     : QObject(feed)
+-    , m_feed(feed)
+-    , m_guid(varHash.value(KeyId).toString())
+-    , m_date(varHash.value(KeyDate).toDateTime())
+-    , m_title(varHash.value(KeyTitle).toString())
+-    , m_author(varHash.value(KeyAuthor).toString())
+-    , m_description(varHash.value(KeyDescription).toString())
+-    , m_torrentURL(varHash.value(KeyTorrentURL).toString())
+-    , m_link(varHash.value(KeyLink).toString())
+-    , m_isRead(varHash.value(KeyIsRead, false).toBool())
+-    , m_data(varHash)
++    , m_feed {feed}
++    , m_guid {varHash.value(KeyId).toString()}
++    , m_date {varHash.value(KeyDate).toDateTime()}
++    , m_title {varHash.value(KeyTitle).toString()}
++    , m_author {varHash.value(KeyAuthor).toString()}
++    , m_description {varHash.value(KeyDescription).toString()}
++    , m_torrentURL {varHash.value(KeyTorrentURL).toString()}
++    , m_link {varHash.value(KeyLink).toString()}
++    , m_isRead {varHash.value(KeyIsRead, false).toBool()}
++    , m_data {varHash}
+ {
+ }
+ 
+--- a/src/gui/rss/rsswidget.cpp
++++ b/src/gui/rss/rsswidget.cpp
+@@ -40,6 +40,7 @@
+ #include <QString>
+ 
+ #include "base/global.h"
++#include "base/logger.h"
+ #include "base/net/downloadmanager.h"
+ #include "base/preferences.h"
+ #include "base/rss/rss_article.h"
+@@ -415,16 +416,52 @@ void RSSWidget::downloadSelectedTorrents
+ // open the url of the selected RSS articles in the Web browser
+ void RSSWidget::openSelectedArticlesUrls()
+ {
++    qsizetype emptyLinkCount = 0;
++    qsizetype badLinkCount = 0;
++    QString articleTitle;
+     for (QListWidgetItem *item : asConst(m_ui->articleListWidget->selectedItems()))
+     {
+         auto *article = item->data(Qt::UserRole).value<RSS::Article *>();
+         Q_ASSERT(article);
+ 
+-        // Mark as read
+         article->markAsRead();
+ 
+-        if (!article->link().isEmpty())
+-            QDesktopServices::openUrl(QUrl(article->link()));
++        const QString articleLink = article->link();
++        const QUrl articleLinkURL {articleLink};
++        if (articleLinkURL.isEmpty()) [[unlikely]]
++        {
++            if (articleTitle.isEmpty())
++                articleTitle = article->title();
++            ++emptyLinkCount;
++        }
++        else if (articleLinkURL.isLocalFile()) [[unlikely]]
++        {
++            if (badLinkCount == 0)
++                articleTitle = article->title();
++            ++badLinkCount;
++
++            LogMsg(tr("Blocked opening RSS article URL. URL pointing to local file might be malicious behaviour. Article: \"%1\". URL: \"%2\".")
++                    .arg(article->title(), articleLink), Log::WARNING);
++        }
++        else [[likely]]
++        {
++            QDesktopServices::openUrl(articleLinkURL);
++        }
++    }
++
++    if (badLinkCount > 0)
++    {
++        QString message = tr("Blocked opening RSS article URL. The following article URL is pointing to local file and it may be malicious behaviour:\n%1").arg(articleTitle);
++        if (badLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more articles with the same issue.").arg(badLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
++    }
++    else if (emptyLinkCount > 0)
++    {
++        QString message = tr("The following article has no news URL provided:\n%1").arg(articleTitle);
++        if (emptyLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more articles with the same issue.").arg(emptyLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
+     }
+ }
+ 
+--- a/src/gui/search/searchjobwidget.cpp
++++ b/src/gui/search/searchjobwidget.cpp
+@@ -35,10 +35,12 @@
+ #include <QHeaderView>
+ #include <QKeyEvent>
+ #include <QMenu>
++#include <QMessageBox>
+ #include <QPalette>
+ #include <QStandardItemModel>
+ #include <QUrl>
+ 
++#include "base/logger.h"
+ #include "base/preferences.h"
+ #include "base/search/searchdownloadhandler.h"
+ #include "base/search/searchhandler.h"
+@@ -319,15 +321,52 @@ void SearchJobWidget::downloadTorrents(c
+         downloadTorrent(rowIndex, option);
+ }
+ 
+-void SearchJobWidget::openTorrentPages() const
++void SearchJobWidget::openTorrentPages()
+ {
+-    const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()};
++    const QModelIndexList rows = m_ui->resultsBrowser->selectionModel()->selectedRows();
++    qsizetype emptyLinkCount = 0;
++    qsizetype badLinkCount = 0;
++    QString warningEntryName;
+     for (const QModelIndex &rowIndex : rows)
+     {
+-        const QString descrLink = m_proxyModel->data(
+-                    m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK)).toString();
+-        if (!descrLink.isEmpty())
+-            QDesktopServices::openUrl(QUrl::fromEncoded(descrLink.toUtf8()));
++        const QString entryName = m_proxyModel->index(rowIndex.row(), SearchSortModel::NAME).data().toString();
++        const QString descrLink = m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK).data().toString();
++
++        const QUrl descrLinkURL {descrLink};
++        if (descrLinkURL.isEmpty()) [[unlikely]]
++        {
++            if (warningEntryName.isEmpty())
++                warningEntryName = entryName;
++            ++emptyLinkCount;
++        }
++        else if (descrLinkURL.isLocalFile()) [[unlikely]]
++        {
++            if (badLinkCount == 0)
++                warningEntryName = entryName;
++            ++badLinkCount;
++
++            LogMsg(tr("Blocked opening search result description page URL. URL pointing to local file might be malicious behaviour. Name: \"%1\". URL: \"%2\".")
++                    .arg(entryName, descrLink), Log::WARNING);
++        }
++        else [[likely]]
++        {
++            QDesktopServices::openUrl(descrLinkURL);
++        }
++    }
++
++    if (badLinkCount > 0)
++    {
++        QString message = tr("Blocked opening search result description page URL. The following result URL is pointing to local file and it may be malicious behaviour:\n%1").arg(warningEntryName);
++        if (badLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more results with the same issue.").arg(badLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
++    }
++    else if (emptyLinkCount > 0)
++    {
++        QString message = tr("Entry \"%1\" has no description page URL provided.").arg(warningEntryName);
++        if (emptyLinkCount > 1)
++            message.append(u"\n" + tr("There are %1 more entries with the same issue.").arg(emptyLinkCount - 1));
++        QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
+     }
+ }
+ 
+--- a/src/gui/search/searchjobwidget.h
++++ b/src/gui/search/searchjobwidget.h
+@@ -127,7 +127,7 @@ private:
+     void onUIThemeChanged();
+ 
+     void downloadTorrents(AddTorrentOption option = AddTorrentOption::Default);
+-    void openTorrentPages() const;
++    void openTorrentPages();
+     void copyTorrentURLs() const;
+     void copyTorrentDownloadLinks() const;
+     void copyTorrentNames() const;
diff -Nru qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch
--- qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch	1970-01-01 01:00:00.000000000 +0100
+++ qbittorrent-5.1.0/debian/patches/9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch	2025-07-06 16:39:44.000000000 +0200
@@ -0,0 +1,37 @@
+From 9b29d37d210f28f46eb4758611d7b06f4603b9d1 Mon Sep 17 00:00:00 2001
+From: Chocobo1 <Chocobo1@users.noreply.github.com>
+Date: Mon, 30 Jun 2025 01:39:03 +0800
+Subject: [PATCH] WebAPI: Trim leading whitespaces on Run External Program
+ fields
+
+Hacked qbt instances may contain malicious script placed in Run External Program and the script
+will attempt to hide itself by adding a lot whitespaces at the start of the command string.
+Users may mistake the field of being empty but is actually not.
+So trim the leading whitespaces to easily expose the malicious script.
+
+Note that GUI already trim the fields and only WebAPI doesn't trim them. This patch will unify
+the behavior.
+Related: https://github.com/qbittorrent/docker-qbittorrent-nox/issues/71#issuecomment-2993567440
+
+PR #22939.
+---
+ src/webui/api/appcontroller.cpp | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+--- a/src/webui/api/appcontroller.cpp
++++ b/src/webui/api/appcontroller.cpp
+@@ -673,12 +673,12 @@ void AppController::setPreferencesAction
+     if (hasKey(u"autorun_on_torrent_added_enabled"_s))
+         pref->setAutoRunOnTorrentAddedEnabled(it.value().toBool());
+     if (hasKey(u"autorun_on_torrent_added_program"_s))
+-        pref->setAutoRunOnTorrentAddedProgram(it.value().toString());
++        pref->setAutoRunOnTorrentAddedProgram(it.value().toString().trimmed());
+     // Run an external program on torrent finished
+     if (hasKey(u"autorun_enabled"_s))
+         pref->setAutoRunOnTorrentFinishedEnabled(it.value().toBool());
+     if (hasKey(u"autorun_program"_s))
+-        pref->setAutoRunOnTorrentFinishedProgram(it.value().toString());
++        pref->setAutoRunOnTorrentFinishedProgram(it.value().toString().trimmed());
+ 
+     // Connection
+     // Listening Port
diff -Nru qbittorrent-5.1.0/debian/patches/series qbittorrent-5.1.0/debian/patches/series
--- qbittorrent-5.1.0/debian/patches/series	1970-01-01 01:00:00.000000000 +0100
+++ qbittorrent-5.1.0/debian/patches/series	2025-07-06 16:39:32.000000000 +0200
@@ -0,0 +1,2 @@
+4f94eac235cefa8b83489cb3135dad87fcbed1e3.patch
+9b29d37d210f28f46eb4758611d7b06f4603b9d1.patch

--- End Message ---
--- Begin Message ---
Unblocked.

--- End Message ---

Reply to: