Compare commits

...

2 commits

Author SHA1 Message Date
advplyr
972193b193 Update server settings authLoginCustomMessage to sanitize on save and load
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Run Component Tests / Run Component Tests (push) Waiting to run
Build and Push Docker Image / build (push) Waiting to run
Integration Test / build and test (push) Waiting to run
Run Unit Tests / Run Unit Tests (push) Waiting to run
2026-03-11 17:18:05 -05:00
advplyr
690a7e0da9 Update session DeviceInfo with sanitize on clientDeviceInfo 2026-03-11 17:03:07 -05:00
5 changed files with 53 additions and 38 deletions

View file

@ -66,7 +66,11 @@
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td> </td>
<td class="hidden sm:table-cell max-w-32 min-w-32"> <td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> <p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td> </td>
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32"> <td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
<p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p> <p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p>
@ -130,7 +134,11 @@
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td> </td>
<td class="hidden sm:table-cell max-w-32 min-w-32"> <td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> <p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td> </td>
<td class="text-center"> <td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
@ -172,7 +180,11 @@
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td> </td>
<td class="hidden sm:table-cell max-w-32 min-w-32"> <td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> <p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td> </td>
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)"> <td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p> <p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
@ -433,16 +445,16 @@ export default {
this.selectedSession = session this.selectedSession = session
this.showSessionModal = true this.showSessionModal = true
}, },
getDeviceInfoString(deviceInfo) { getDeviceInfoLines(deviceInfo) {
if (!deviceInfo) return '' if (!deviceInfo) return []
var lines = [] const lines = []
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
if (deviceInfo.browserName) lines.push(deviceInfo.browserName) if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
return lines.join('<br>') return lines
}, },
getPlayMethodName(playMethod) { getPlayMethodName(playMethod) {
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'

View file

@ -38,8 +38,12 @@
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td> </td>
<td class="hidden sm:table-cell min-w-32 max-w-32"> <td class="hidden sm:table-cell min-w-32 max-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> <p class="text-xs truncate">
</td> <template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td>
<td class="text-center"> <td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p> <p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p>
</td> </td>
@ -193,16 +197,16 @@ export default {
this.selectedSession = session this.selectedSession = session
this.showSessionModal = true this.showSessionModal = true
}, },
getDeviceInfoString(deviceInfo) { getDeviceInfoLines(deviceInfo) {
if (!deviceInfo) return '' if (!deviceInfo) return []
var lines = [] const lines = []
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
if (deviceInfo.browserName) lines.push(deviceInfo.browserName) if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
return lines.join('<br>') return lines
}, },
getPlayMethodName(playMethod) { getPlayMethodName(playMethod) {
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'

View file

@ -1,6 +1,10 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require('uuid').v4
const { stripAllTags } = require('../utils/htmlSanitizer')
class DeviceInfo { class DeviceInfo {
/** @type {string[]} Fields to sanitize when loading from stored data */
static stringFields = ['deviceId', 'clientVersion', 'manufacturer', 'model', 'sdkVersion', 'clientName', 'deviceName']
constructor(deviceInfo = null) { constructor(deviceInfo = null) {
this.id = null this.id = null
this.userId = null this.userId = null
@ -31,7 +35,7 @@ class DeviceInfo {
construct(deviceInfo) { construct(deviceInfo) {
for (const key in deviceInfo) { for (const key in deviceInfo) {
if (deviceInfo[key] !== undefined && this[key] !== undefined) { if (deviceInfo[key] !== undefined && this[key] !== undefined) {
this[key] = deviceInfo[key] this[key] = DeviceInfo.stringFields.includes(key) ? stripAllTags(deviceInfo[key]) : deviceInfo[key]
} }
} }
} }
@ -63,7 +67,8 @@ class DeviceInfo {
} }
get deviceDescription() { get deviceDescription() {
if (this.model) { // Set from mobile apps if (this.model) {
// Set from mobile apps
if (this.sdkVersion) return `${this.model} SDK ${this.sdkVersion} / v${this.clientVersion}` if (this.sdkVersion) return `${this.model} SDK ${this.sdkVersion} / v${this.clientVersion}`
return `${this.model} / v${this.clientVersion}` return `${this.model} / v${this.clientVersion}`
} }
@ -72,18 +77,7 @@ class DeviceInfo {
// When client doesn't send a device id // When client doesn't send a device id
getTempDeviceId() { getTempDeviceId() {
const keys = [ const keys = [this.userId, this.browserName, this.browserVersion, this.osName, this.osVersion, this.clientVersion, this.manufacturer, this.model, this.sdkVersion, this.ipAddress].map((k) => k || '')
this.userId,
this.browserName,
this.browserVersion,
this.osName,
this.osVersion,
this.clientVersion,
this.manufacturer,
this.model,
this.sdkVersion,
this.ipAddress
].map(k => k || '')
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
} }
@ -99,12 +93,12 @@ class DeviceInfo {
this.osVersion = ua?.os.version || null this.osVersion = ua?.os.version || null
this.deviceType = ua?.device.type || null this.deviceType = ua?.device.type || null
this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion this.clientVersion = stripAllTags(clientDeviceInfo?.clientVersion) || serverVersion
this.manufacturer = clientDeviceInfo?.manufacturer || null this.manufacturer = stripAllTags(clientDeviceInfo?.manufacturer) || null
this.model = clientDeviceInfo?.model || null this.model = stripAllTags(clientDeviceInfo?.model) || null
this.sdkVersion = clientDeviceInfo?.sdkVersion || null this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null
this.clientName = clientDeviceInfo?.clientName || null this.clientName = stripAllTags(clientDeviceInfo?.clientName) || null
if (this.sdkVersion) { if (this.sdkVersion) {
if (!this.clientName) this.clientName = 'Abs Android' if (!this.clientName) this.clientName = 'Abs Android'
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}` this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
@ -149,4 +143,4 @@ class DeviceInfo {
return hasUpdates return hasUpdates
} }
} }
module.exports = DeviceInfo module.exports = DeviceInfo

View file

@ -3,6 +3,7 @@ const packageJson = require('../../../package.json')
const { BookshelfView } = require('../../utils/constants') const { BookshelfView } = require('../../utils/constants')
const Logger = require('../../Logger') const Logger = require('../../Logger')
const User = require('../../models/User') const User = require('../../models/User')
const { sanitize } = require('../../utils/htmlSanitizer')
class ServerSettings { class ServerSettings {
constructor(settings) { constructor(settings) {
@ -126,7 +127,7 @@ class ServerSettings {
this.version = settings.version || null this.version = settings.version || null
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0 this.authLoginCustomMessage = sanitize(settings.authLoginCustomMessage) || null // Added v2.8.0
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null
@ -309,7 +310,7 @@ class ServerSettings {
get authFormData() { get authFormData() {
const clientFormData = { const clientFormData = {
authLoginCustomMessage: this.authLoginCustomMessage authLoginCustomMessage: sanitize(this.authLoginCustomMessage)
} }
if (this.authActiveAuthMethods.includes('openid')) { if (this.authActiveAuthMethods.includes('openid')) {
clientFormData.authOpenIDButtonText = this.authOpenIDButtonText clientFormData.authOpenIDButtonText = this.authOpenIDButtonText
@ -327,6 +328,9 @@ class ServerSettings {
update(payload) { update(payload) {
let hasUpdates = false let hasUpdates = false
for (const key in payload) { for (const key in payload) {
if (key === 'authLoginCustomMessage') {
payload[key] = sanitize(payload[key])
}
if (key === 'sortingPrefixes') { if (key === 'sortingPrefixes') {
// Sorting prefixes are updated with the /api/sorting-prefixes endpoint // Sorting prefixes are updated with the /api/sorting-prefixes endpoint
continue continue

View file

@ -5,11 +5,10 @@ const { entities } = require('./htmlEntities')
* *
* @param {string} html * @param {string} html
* @returns {string} * @returns {string}
* @throws {Error} if input is not a string
*/ */
function sanitize(html) { function sanitize(html) {
if (typeof html !== 'string') { if (typeof html !== 'string') {
throw new Error('sanitizeHtml: input must be a string') return ''
} }
const sanitizerOptions = { const sanitizerOptions = {
@ -27,6 +26,8 @@ function sanitize(html) {
module.exports.sanitize = sanitize module.exports.sanitize = sanitize
function stripAllTags(html, shouldDecodeEntities = true) { function stripAllTags(html, shouldDecodeEntities = true) {
if (typeof html !== 'string') return ''
const sanitizerOptions = { const sanitizerOptions = {
allowedTags: [], allowedTags: [],
disallowedTagsMode: 'discard' disallowedTagsMode: 'discard'