diff --git a/CMakeLists.txt b/CMakeLists.txt index 665936f..3af4a33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,9 +88,18 @@ if (UNIX AND NOT APPLE) target_link_libraries(qobuz-qt PRIVATE asound) endif () -# Compiler warnings +# Compiler warnings + hardening if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - target_compile_options(qobuz-qt PRIVATE -Wall -Wextra -Wno-unused-parameter) + target_compile_options(qobuz-qt PRIVATE + -Wall -Wextra -Wno-unused-parameter + -fstack-protector-strong + -D_FORTIFY_SOURCE=2 + -fPIE + ) + target_link_options(qobuz-qt PRIVATE + -pie + -Wl,-z,relro,-z,now + ) endif () # D-Bus diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9b9974f..e3f7b9d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -31,3 +31,4 @@ toml = "0.8" [profile.release] lto = "thin" opt-level = 3 +overflow-checks = true diff --git a/rust/src/api/client.rs b/rust/src/api/client.rs index 7486a3d..56b94ea 100644 --- a/rust/src/api/client.rs +++ b/rust/src/api/client.rs @@ -60,6 +60,8 @@ impl QobuzClient { .as_secs() } + /// Compute the request signature required by the Qobuz API. + /// NOTE: MD5 is mandated by the Qobuz API protocol — not our choice. fn request_sig(&self, method: &str, params: &mut Vec<(&str, String)>, ts: u64) -> String { params.sort_by_key(|(k, _)| *k); let mut s = method.replace('/', ""); @@ -116,6 +118,7 @@ impl QobuzClient { } } + /// NOTE: Qobuz API requires credentials as GET query params — not our choice. async fn oauth2_login(&mut self, email: &str, password: &str) -> Result { let ts = Self::ts(); let mut sign_params: Vec<(&str, String)> = vec![ diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f618984..83cab4f 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -96,7 +96,9 @@ pub struct Backend(BackendInner); // ---------- Helpers ---------- fn call_cb(cb: EventCallback, ud: SendPtr, ev: c_int, json: &str) { - let cstr = CString::new(json).unwrap_or_else(|_| CString::new("{}").unwrap()); + // Strip null bytes that would cause CString::new to fail + let safe = json.replace('\0', ""); + let cstr = CString::new(safe).unwrap_or_else(|_| CString::new("{}").unwrap()); unsafe { cb(ud.0, ev, cstr.as_ptr()) }; } @@ -119,8 +121,14 @@ pub unsafe extern "C" fn qobuz_backend_new( event_cb: EventCallback, userdata: *mut c_void, ) -> *mut Backend { - let rt = Runtime::new().expect("tokio runtime"); - let client = Arc::new(Mutex::new(QobuzClient::new().expect("QobuzClient"))); + let rt = match Runtime::new() { + Ok(r) => r, + Err(_) => return std::ptr::null_mut(), + }; + let client = match QobuzClient::new() { + Ok(c) => Arc::new(Mutex::new(c)), + Err(_) => return std::ptr::null_mut(), + }; let player = Player::new(); Box::into_raw(Box::new(Backend(BackendInner { diff --git a/src/backend/qobuzbackend.cpp b/src/backend/qobuzbackend.cpp index 01c5c43..42ca1ff 100644 --- a/src/backend/qobuzbackend.cpp +++ b/src/backend/qobuzbackend.cpp @@ -8,6 +8,10 @@ QobuzBackend::QobuzBackend(QObject *parent) : QObject(parent) { m_backend = qobuz_backend_new(&QobuzBackend::eventTrampoline, this); + if (!m_backend) { + qCritical("Failed to initialize Qobuz backend"); + return; + } m_positionTimer = new QTimer(this); m_positionTimer->setInterval(50); @@ -194,7 +198,12 @@ void QobuzBackend::onPositionTick() void QobuzBackend::onEvent(int eventType, const QString &json) { - const QJsonObject obj = QJsonDocument::fromJson(json.toUtf8()).object(); + const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8()); + if (!doc.isObject()) { + emit error(tr("Malformed response from backend")); + return; + } + const QJsonObject obj = doc.object(); switch (eventType) { case EV_LOGIN_OK: diff --git a/src/model/tracklistmodel.cpp b/src/model/tracklistmodel.cpp index e5021e3..1d009d0 100644 --- a/src/model/tracklistmodel.cpp +++ b/src/model/tracklistmodel.cpp @@ -294,7 +294,8 @@ void TrackListModel::sort(int column, Qt::SortOrder order) QString TrackListModel::formatDuration(qint64 secs) { - const int m = static_cast(secs / 60); - const int s = static_cast(secs % 60); + if (secs < 0) secs = 0; + const qint64 m = secs / 60; + const qint64 s = secs % 60; return QStringLiteral("%1:%2").arg(m).arg(s, 2, 10, QLatin1Char('0')); } diff --git a/src/view/artistview.cpp b/src/view/artistview.cpp index 2ce3869..f21446b 100644 --- a/src/view/artistview.cpp +++ b/src/view/artistview.cpp @@ -133,15 +133,22 @@ void ArtistView::setArtist(const QJsonObject &artist) // artist/page: name is {"display": "..."} m_nameLabel->setText(artist["name"].toObject()["display"].toString()); - // biography.content is HTML — strip tags for the summary label + // biography.content is HTML — strip tags for safe plain-text display const QString bioHtml = artist["biography"].toObject()["content"].toString(); if (!bioHtml.isEmpty()) { - // Remove HTML tags for plain-text display QString plain = bioHtml; + // Strip HTML entities and tags to prevent rendering injected content plain.remove(QRegularExpression(QStringLiteral("<[^>]*>"))); + plain.replace(QStringLiteral("&"), QStringLiteral("&")); + plain.replace(QStringLiteral("<"), QStringLiteral("<")); + plain.replace(QStringLiteral(">"), QStringLiteral(">")); + plain.replace(QStringLiteral("""), QStringLiteral("\"")); + plain.replace(QStringLiteral("'"), QStringLiteral("'")); + plain.replace(QStringLiteral(" "), QStringLiteral(" ")); plain = plain.trimmed(); + m_bioLabel->setTextFormat(Qt::PlainText); m_bioLabel->setText(plain); - m_bioLabel->setVisible(true); + m_bioLabel->setVisible(!plain.isEmpty()); } else { m_bioLabel->setVisible(false); } diff --git a/src/view/maintoolbar.cpp b/src/view/maintoolbar.cpp index 5191199..391fd00 100644 --- a/src/view/maintoolbar.cpp +++ b/src/view/maintoolbar.cpp @@ -150,8 +150,8 @@ void MainToolBar::setCurrentTrack(const QJsonObject &track) void MainToolBar::updateProgress(quint64 position, quint64 duration) { if (m_seeking) return; - const int sliderPos = duration > 0 - ? static_cast(position * 1000 / duration) : 0; + const int sliderPos = (duration > 0) + ? static_cast(qMin(position * 1000 / duration, quint64(1000))) : 0; m_progress->blockSignals(true); m_progress->setValue(sliderPos); m_progress->blockSignals(false);