From 74cabebc4b35626cf99e8fafeeb877eb0cac652a Mon Sep 17 00:00:00 2001 From: Sebastian Palencsar Date: Sat, 20 Jun 2026 14:31:13 +0200 Subject: [PATCH] release: 1.0.4 security hardening and stability fixes Validate manual stream URLs (http/https only), restrict notification cover downloads to HTTPS, and add transfer timeouts for metadata and cover fetches. Document playback, tray, MPRIS, and QML refactor changes in CHANGELOG and AppStream metadata. --- CHANGELOG.md | 24 ++++++++++++++++++++++++ CMakeLists.txt | 2 +- de.nerdbear.bearwave.metainfo.xml | 10 ++++++++++ src/coverartfetcher.cpp | 1 + src/icyreader.cpp | 1 + src/main.cpp | 2 +- src/notificationmanager.cpp | 6 ++++++ src/radiobackend.cpp | 21 +++++++++++++++++++++ tests/radiobackend_playback_test.cpp | 26 ++++++++++++++++++++++++++ 9 files changed, 91 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d679ea0..d241a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.4] - 2026-06-20 + +### Security + +- Validate manual station URLs: only `http://` and `https://` schemes are accepted. +- Restrict notification cover art downloads to HTTPS URLs. +- Add network transfer timeouts to ICY metadata, cover art, and notification fetches. + +### Fixed + +- Fix playback index for history, resume, and next/previous navigation. +- Fix Radio Browser API race where stale responses could overwrite newer results. +- Suppress error banner when cached station data is still available. +- Fix search filter persisting across page changes and compact-mode debounce. +- Fix stale list index after station list reload. +- Fix Wayland system tray menu using native `QSystemTrayIcon`. +- Fix MPRIS metadata publishing for desktop media widgets (e.g. PlasMusic). + +### Changed + +- Refactor monolithic `Main.qml` into reusable QML components and `BearTheme`. +- Remove unused Kirigami, I18n, and CoreAddons dependencies. +- Add backend unit tests for playback navigation and API race handling. + ## [1.0.3] - 2026-05-30 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index bf4dfaf..f221975 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -project(bearwave VERSION 1.0.3 LANGUAGES CXX) +project(bearwave VERSION 1.0.4 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/de.nerdbear.bearwave.metainfo.xml b/de.nerdbear.bearwave.metainfo.xml index 5708bac..058eb21 100644 --- a/de.nerdbear.bearwave.metainfo.xml +++ b/de.nerdbear.bearwave.metainfo.xml @@ -59,6 +59,16 @@ + + +
    +
  • Security: validate manual stream URLs (http/https only) and HTTPS-only cover downloads
  • +
  • Security: add network transfer timeouts for metadata and cover fetches
  • +
  • Fixed playback navigation, API race conditions, Wayland tray, and MPRIS metadata
  • +
  • Refactored QML UI into modular components
  • +
+
+
    diff --git a/src/coverartfetcher.cpp b/src/coverartfetcher.cpp index 53eb424..64ce41a 100644 --- a/src/coverartfetcher.cpp +++ b/src/coverartfetcher.cpp @@ -42,6 +42,7 @@ void CoverArtFetcher::fetch(const QString &artist, const QString &title) url.setQuery(query); QNetworkRequest request(url); + request.setTransferTimeout(10000); m_currentReply = m_networkManager->get(request); connect(m_currentReply, &QNetworkReply::finished, this, &CoverArtFetcher::onReplyFinished); } diff --git a/src/icyreader.cpp b/src/icyreader.cpp index 27c5bba..f97976a 100644 --- a/src/icyreader.cpp +++ b/src/icyreader.cpp @@ -30,6 +30,7 @@ void IcyReader::start(const QString &url) request.setRawHeader("User-Agent", "VLC/3.0.16 LibVLC/3.0.16"); // Streams often use 302 redirects to load balancers/relays request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + request.setTransferTimeout(15000); m_reply = m_nam->get(request); diff --git a/src/main.cpp b/src/main.cpp index cfbc488..f47b813 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,7 +25,7 @@ int main(int argc, char *argv[]) QApplication::setApplicationName(QStringLiteral("BearWave")); QApplication::setDesktopFileName(QStringLiteral("de.nerdbear.bearwave")); QApplication::setOrganizationName(QStringLiteral("BearWave")); - QApplication::setApplicationVersion(QStringLiteral("1.0.3")); + QApplication::setApplicationVersion(QStringLiteral("1.0.4")); QApplication app(argc, argv); diff --git a/src/notificationmanager.cpp b/src/notificationmanager.cpp index 4a26cae..25cd80c 100644 --- a/src/notificationmanager.cpp +++ b/src/notificationmanager.cpp @@ -123,6 +123,11 @@ void NotificationManager::onTrackInfoChanged() void NotificationManager::downloadCover(const QString &url) { + if (!url.startsWith(QLatin1String("https://"))) { + qDebug() << "Skipping cover download for non-HTTPS URL"; + return; + } + if (m_currentReply) { QNetworkReply *reply = m_currentReply; m_currentReply = nullptr; @@ -133,6 +138,7 @@ void NotificationManager::downloadCover(const QString &url) m_pendingCoverUrl = url; QNetworkRequest request(url); + request.setTransferTimeout(10000); m_currentReply = m_networkManager->get(request); connect(m_currentReply, &QNetworkReply::finished, this, &NotificationManager::onDownloadFinished); } diff --git a/src/radiobackend.cpp b/src/radiobackend.cpp index db2f0d0..b7dd16a 100644 --- a/src/radiobackend.cpp +++ b/src/radiobackend.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace { constexpr int kRecentLimit = 20; @@ -20,6 +21,16 @@ QString appConfigDir() { return QDir::homePath() + QStringLiteral("/.config/bearwave"); } + +bool isAllowedStreamUrl(const QString &urlString) +{ + const QUrl url(urlString.trimmed()); + if (!url.isValid() || url.isEmpty()) { + return false; + } + const QString scheme = url.scheme().toLower(); + return scheme == QStringLiteral("http") || scheme == QStringLiteral("https"); +} } RadioBackend::RadioBackend(QObject *parent) @@ -409,7 +420,12 @@ void RadioBackend::addManualStation(const QString &name, const QString &url, con if (name.trimmed().isEmpty() || url.trimmed().isEmpty()) { return; } + if (!isAllowedStreamUrl(url)) { + setLastError(tr("Stream URL must use http:// or https://")); + return; + } + setLastError(QString()); RadioStation *station = new RadioStation(this); station->setName(name.trimmed()); station->setUrl(url.trimmed()); @@ -848,7 +864,12 @@ void RadioBackend::editManualStation(QObject *stationObj, const QString &name, c if (!station || name.trimmed().isEmpty() || url.trimmed().isEmpty()) { return; } + if (!isAllowedStreamUrl(url)) { + setLastError(tr("Stream URL must use http:// or https://")); + return; + } + setLastError(QString()); QString oldUrl = station->url(); QString oldUrlResolved = station->urlResolved(); diff --git a/tests/radiobackend_playback_test.cpp b/tests/radiobackend_playback_test.cpp index 74be22b..c97b067 100644 --- a/tests/radiobackend_playback_test.cpp +++ b/tests/radiobackend_playback_test.cpp @@ -29,6 +29,8 @@ private slots: void cached_load_keeps_error_clear_after_network_failure(); void list_reload_resyncs_index_for_playing_station(); void list_next_disabled_when_playing_station_not_in_new_list(); + void manual_station_rejects_unsafe_url_scheme(); + void manual_station_accepts_http_and_https(); }; namespace { @@ -302,5 +304,29 @@ void RadioBackendPlaybackTest::list_next_disabled_when_playing_station_not_in_ne QCOMPARE(backend.currentStationUrl(), topUrl); } +void RadioBackendPlaybackTest::manual_station_rejects_unsafe_url_scheme() +{ + RadioBackend backend; + + backend.addManualStation(QStringLiteral("Unsafe"), QStringLiteral("file:///etc/passwd"), QStringLiteral("DE")); + QCOMPARE(backend.stations().size(), 0); + QVERIFY(!backend.lastError().isEmpty()); + + backend.addManualStation(QStringLiteral("FTP"), QStringLiteral("ftp://example.com/stream"), QStringLiteral("DE")); + QCOMPARE(backend.stations().size(), 0); +} + +void RadioBackendPlaybackTest::manual_station_accepts_http_and_https() +{ + RadioBackend backend; + + backend.addManualStation(QStringLiteral("HTTP"), QStringLiteral("http://example.com/stream"), QStringLiteral("DE")); + QCOMPARE(backend.stations().size(), 1); + QCOMPARE(backend.lastError(), QString()); + + backend.addManualStation(QStringLiteral("HTTPS"), QStringLiteral("https://example.com/stream"), QStringLiteral("DE")); + QCOMPARE(backend.stations().size(), 2); +} + QTEST_MAIN(RadioBackendPlaybackTest) #include "radiobackend_playback_test.moc" \ No newline at end of file