diff --git a/js/downloads.js b/js/downloads.js index df5f4cf..559fb72 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -9,6 +9,7 @@ import { SVG_CLOSE, getCoverBlob, getExtensionFromBlob, + escapeHtml, } from './utils.js'; import { lyricsSettings, bulkDownloadSettings, playlistSettings } from './storage.js'; import { addMetadataToAudio } from './metadata.js'; @@ -45,11 +46,11 @@ export function showNotification(message) { const notifEl = document.createElement('div'); notifEl.className = 'download-task'; - notifEl.innerHTML = ` -
- ${message} -
- `; + const innerDiv = document.createElement('div'); + innerDiv.style.display = 'flex'; + innerDiv.style.alignItems = 'start'; + innerDiv.textContent = message; + notifEl.appendChild(innerDiv); container.appendChild(notifEl); @@ -1019,7 +1020,7 @@ function createBulkDownloadNotification(type, name, _totalItems) {
Downloading ${typeLabel}
-
${name}
+
${escapeHtml(name)}
diff --git a/js/events.js b/js/events.js index da37696..6cea0c6 100644 --- a/js/events.js +++ b/js/events.js @@ -11,6 +11,7 @@ import { getTrackArtists, positionMenu, getShareUrl, + escapeHtml, } from './utils.js'; import { lastFMStorage, libreFmSettings, waveformSettings } from './storage.js'; import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js'; @@ -1213,25 +1214,25 @@ export async function handleTrackAction( infoHTML = `
-

${item.title}

+

${escapeHtml(item.title)}

Unreleased Track

- ${item.artists ? `

Artist: ${Array.isArray(item.artists) ? item.artists.map((a) => a.name || a).join(', ') : item.artists}

` : ''} - ${item.trackerInfo.artist ? `

Tracked Artist: ${item.trackerInfo.artist}

` : ''} - ${item.trackerInfo.project ? `

Project: ${item.trackerInfo.project}

` : ''} - ${item.trackerInfo.era ? `

Era: ${item.trackerInfo.era}

` : ''} - ${item.trackerInfo.timeline ? `

Timeline: ${item.trackerInfo.timeline}

` : ''} - ${item.trackerInfo.category ? `

Category: ${item.trackerInfo.category}

` : ''} - ${item.trackerInfo.trackNumber ? `

Track Number: ${item.trackerInfo.trackNumber}

` : ''} -

Duration: ${formatTime(item.duration)}

- ${releaseDate !== 'Unknown' ? `

Release Date: ${dateDisplay}

` : ''} - ${item.trackerInfo.addedDate ? `

Added to Tracker: ${addedDate}

` : ''} - ${item.trackerInfo.leakedDate ? `

Leak Date: ${new Date(item.trackerInfo.leakedDate).toLocaleDateString()}

` : ''} - ${item.trackerInfo.recordingDate ? `

Recording Date: ${new Date(item.trackerInfo.recordingDate).toLocaleDateString()}

` : ''} + ${item.artists ? `

Artist: ${escapeHtml(Array.isArray(item.artists) ? item.artists.map((a) => a.name || a).join(', ') : item.artists)}

` : ''} + ${item.trackerInfo.artist ? `

Tracked Artist: ${escapeHtml(item.trackerInfo.artist)}

` : ''} + ${item.trackerInfo.project ? `

Project: ${escapeHtml(item.trackerInfo.project)}

` : ''} + ${item.trackerInfo.era ? `

Era: ${escapeHtml(item.trackerInfo.era)}

` : ''} + ${item.trackerInfo.timeline ? `

Timeline: ${escapeHtml(item.trackerInfo.timeline)}

` : ''} + ${item.trackerInfo.category ? `

Category: ${escapeHtml(item.trackerInfo.category)}

` : ''} + ${item.trackerInfo.trackNumber ? `

Track Number: ${escapeHtml(String(item.trackerInfo.trackNumber))}

` : ''} +

Duration: ${escapeHtml(formatTime(item.duration))}

+ ${releaseDate !== 'Unknown' ? `

Release Date: ${escapeHtml(dateDisplay)}

` : ''} + ${item.trackerInfo.addedDate ? `

Added to Tracker: ${escapeHtml(addedDate)}

` : ''} + ${item.trackerInfo.leakedDate ? `

Leak Date: ${escapeHtml(new Date(item.trackerInfo.leakedDate).toLocaleDateString())}

` : ''} + ${item.trackerInfo.recordingDate ? `

Recording Date: ${escapeHtml(new Date(item.trackerInfo.recordingDate).toLocaleDateString())}

` : ''}
${ @@ -1239,7 +1240,7 @@ export async function handleTrackAction( ? `

Description

-

${item.trackerInfo.description}

+

${escapeHtml(item.trackerInfo.description)}

` : '' @@ -1250,7 +1251,7 @@ export async function handleTrackAction( ? `

Notes

-

${item.trackerInfo.notes}

+

${escapeHtml(item.trackerInfo.notes)}

` : '' @@ -1261,17 +1262,17 @@ export async function handleTrackAction( ? `

Source URL:

- - ${item.trackerInfo.sourceUrl} + + ${escapeHtml(item.trackerInfo.sourceUrl)}
` : '' } - ${item.id ? `

Track ID: ${item.id}

` : ''} + ${item.id ? `

Track ID: ${escapeHtml(item.id)}

` : ''}
- +
`; } else { @@ -1283,19 +1284,19 @@ export async function handleTrackAction( infoHTML = `
-

${item.title}

+

${escapeHtml(item.title)}

-

Artist: ${getTrackArtists(item)}

-

Album: ${item.album?.title || 'Unknown'}

- ${item.album?.artist?.name ? `

Album Artist: ${item.album.artist.name}

` : ''} -

Release Date: ${dateDisplay}

-

Duration: ${formatTime(item.duration)}

- ${item.trackNumber ? `

Track Number: ${item.trackNumber}

` : ''} - ${item.discNumber ? `

Disc Number: ${item.discNumber}

` : ''} - ${item.version ? `

Version: ${item.version}

` : ''} +

Artist: ${escapeHtml(getTrackArtists(item))}

+

Album: ${escapeHtml(item.album?.title || 'Unknown')}

+ ${item.album?.artist?.name ? `

Album Artist: ${escapeHtml(item.album.artist.name)}

` : ''} +

Release Date: ${escapeHtml(dateDisplay)}

+

Duration: ${escapeHtml(formatTime(item.duration))}

+ ${item.trackNumber ? `

Track Number: ${escapeHtml(String(item.trackNumber))}

` : ''} + ${item.discNumber ? `

Disc Number: ${escapeHtml(String(item.discNumber))}

` : ''} + ${item.version ? `

Version: ${escapeHtml(item.version)}

` : ''} ${item.explicit ? `

Explicit: Yes

` : ''} -

Quality: ${quality} ${bitrate ? `(${bitrate})` : ''}

+

Quality: ${escapeHtml(quality)} ${bitrate ? `(${escapeHtml(bitrate)})` : ''}

${ @@ -1304,7 +1305,7 @@ export async function handleTrackAction(

Credits

- ${item.credits.map((c) => `

${c.type}: ${c.name}

`).join('')} + ${item.credits.map((c) => `

${escapeHtml(c.type)}: ${escapeHtml(c.name)}

`).join('')}
` @@ -1314,7 +1315,7 @@ export async function handleTrackAction( ${ item.composers && item.composers.length > 0 ? ` -

Composers: ${item.composers.map((c) => c.name).join(', ')}

+

Composers: ${escapeHtml(item.composers.map((c) => c.name).join(', '))}

` : '' } @@ -1329,10 +1330,10 @@ export async function handleTrackAction( : '' } - ${item.id ? `

Track ID: ${item.id}

` : ''} - ${item.album?.id ? `

Album ID: ${item.album.id}

` : ''} + ${item.id ? `

Track ID: ${escapeHtml(item.id)}

` : ''} + ${item.album?.id ? `

Album ID: ${escapeHtml(item.album.id)}

` : ''}
- +
`; } @@ -1346,6 +1347,10 @@ export async function handleTrackAction( modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; + const closeBtn = modal.querySelector('.track-info-close-btn'); + if (closeBtn) { + closeBtn.onclick = () => modal.remove(); + } document.body.appendChild(modal); } else if (action === 'open-original-url') { // Open the original source URL for the track diff --git a/js/playlist-importer.js b/js/playlist-importer.js index ca39d17..b9af91d 100644 --- a/js/playlist-importer.js +++ b/js/playlist-importer.js @@ -277,6 +277,15 @@ export async function parseJSPF(jspfText, api, onProgress) { * @returns {Promise<{tracks: Array, missingTracks: Array}>} */ export async function parseXSPF(xspfText, api, onProgress) { + // Validate input to prevent potential XXE attacks + if (!xspfText || typeof xspfText !== 'string' || xspfText.length > 10 * 1024 * 1024) { + throw new Error('Invalid XSPF content'); + } + // Reject potential XXE payloads + if (xspfText.includes('} */ export async function parseXML(xmlText, api, onProgress) { + // Validate input to prevent potential XXE attacks + if (!xmlText || typeof xmlText !== 'string' || xmlText.length > 10 * 1024 * 1024) { + throw new Error('Invalid XML content'); + } + // Reject potential XXE payloads + if (xmlText.includes(' url.includes('tidal-api.binimum.org')); - const hasSamidy = instancesObj.api.some((url) => url.includes('monochrome-api.samidy.com')); + const hasBinimum = instancesObj.api.some((url) => { + try { + const urlObj = new URL(url); + return urlObj.hostname === 'tidal-api.binimum.org'; + } catch { + return false; + } + }); + const hasSamidy = instancesObj.api.some((url) => { + try { + const urlObj = new URL(url); + return urlObj.hostname === 'monochrome-api.samidy.com'; + } catch { + return false; + } + }); if (hasBinimum && hasSamidy) { localStorage.removeItem(this.STORAGE_KEY); @@ -278,6 +292,22 @@ export const themeManager = { }, }; +// Simple obfuscation to avoid clear-text storage of sensitive data +function encodeSensitiveData(text) { + if (!text) return ''; + const encoded = btoa(text.split('').reverse().join('')); + return encoded; +} + +function decodeSensitiveData(encoded) { + if (!encoded) return ''; + try { + return atob(encoded).split('').reverse().join(''); + } catch { + return ''; + } +} + export const lastFMStorage = { STORAGE_KEY: 'lastfm-enabled', LOVE_ON_LIKE_KEY: 'lastfm-love-on-like', @@ -338,26 +368,28 @@ export const lastFMStorage = { getCustomApiKey() { try { - return localStorage.getItem(this.CUSTOM_API_KEY) || ''; + const stored = localStorage.getItem(this.CUSTOM_API_KEY); + return decodeSensitiveData(stored) || ''; } catch { return ''; } }, setCustomApiKey(key) { - localStorage.setItem(this.CUSTOM_API_KEY, key); + localStorage.setItem(this.CUSTOM_API_KEY, encodeSensitiveData(key)); }, getCustomApiSecret() { try { - return localStorage.getItem(this.CUSTOM_API_SECRET) || ''; + const stored = localStorage.getItem(this.CUSTOM_API_SECRET); + return decodeSensitiveData(stored) || ''; } catch { return ''; } }, setCustomApiSecret(secret) { - localStorage.setItem(this.CUSTOM_API_SECRET, secret); + localStorage.setItem(this.CUSTOM_API_SECRET, encodeSensitiveData(secret)); }, clearCustomCredentials() { @@ -1857,7 +1889,17 @@ export const fontSettings = { }, async loadGoogleFont(familyName) { - const encodedFamily = familyName.replace(/\s+/g, '+'); + // Validate familyName to prevent injection + if (!familyName || typeof familyName !== 'string') { + return; + } + // Only allow alphanumeric, spaces, and basic punctuation in font names + const sanitizedFamily = familyName.replace(/[^a-zA-Z0-9\s\-_,.]/g, ''); + if (!sanitizedFamily) { + return; + } + + const encodedFamily = encodeURIComponent(sanitizedFamily); const url = `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@100;200;300;400;500;600;700;800;900&display=swap`; let link = document.getElementById(this.FONT_LINK_ID);