fix: security hardening and code quality improvements

Build hardening:
- Add -fstack-protector-strong, -D_FORTIFY_SOURCE=2, PIE, full RELRO
- Enable overflow-checks in Rust release profile

Rust backend:
- Return null (not panic) if Tokio runtime or QobuzClient init fails
- Strip null bytes in FFI JSON callback to prevent CString panics
- Document MD5 and password-in-query as Qobuz API constraints

C++ frontend:
- Validate JSON document before accessing fields in onEvent()
- Handle null backend pointer from failed init
- Set biography label to PlainText and decode HTML entities to prevent
  rendering injected content from API responses
- Clamp slider position and guard negative durations
- Use qint64 for duration formatting to avoid int truncation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-24 23:19:04 +01:00
parent eb5c151d3a
commit 5bda2396d1
8 changed files with 51 additions and 13 deletions

View File

@@ -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:

View File

@@ -294,7 +294,8 @@ void TrackListModel::sort(int column, Qt::SortOrder order)
QString TrackListModel::formatDuration(qint64 secs)
{
const int m = static_cast<int>(secs / 60);
const int s = static_cast<int>(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'));
}

View File

@@ -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("&amp;"), QStringLiteral("&"));
plain.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
plain.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
plain.replace(QStringLiteral("&quot;"), QStringLiteral("\""));
plain.replace(QStringLiteral("&#39;"), QStringLiteral("'"));
plain.replace(QStringLiteral("&nbsp;"), 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);
}

View File

@@ -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<int>(position * 1000 / duration) : 0;
const int sliderPos = (duration > 0)
? static_cast<int>(qMin(position * 1000 / duration, quint64(1000))) : 0;
m_progress->blockSignals(true);
m_progress->setValue(sliderPos);
m_progress->blockSignals(false);