From a54f56834d9bee40a540ad8764d5f70d786c5742 Mon Sep 17 00:00:00 2001 From: Samu Date: Sat, 29 Aug 2026 16:01:07 +0200 Subject: [PATCH] fix: classify invalid subsonic URL as unreachable, type settings handlers, guard token mask and body types --- server/src/routes/settingsRoutes.ts | 16 +++++++++++++--- server/src/subsonic.ts | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/server/src/routes/settingsRoutes.ts b/server/src/routes/settingsRoutes.ts index 0a7e414..606c5e4 100644 --- a/server/src/routes/settingsRoutes.ts +++ b/server/src/routes/settingsRoutes.ts @@ -1,6 +1,7 @@ import { FastifyInstance } from 'fastify' import { SubsonicClient } from '../subsonic.js' import { requireAuth } from './authRoutes.js' +import type { UserRow } from '../auth.js' import type { DB } from '../db.js' export interface SettingsRow { @@ -23,7 +24,11 @@ export function getSettings(db: DB, userId: number): SettingsRow { export function settingsView(s: SettingsRow) { return { hasDiscogsToken: !!s.discogs_token, - discogsTokenMasked: s.discogs_token ? `****${s.discogs_token.slice(-5)}` : null, + discogsTokenMasked: s.discogs_token + ? s.discogs_token.length > 5 + ? `****${s.discogs_token.slice(-5)}` + : '****' + : null, subsonicUrl: s.subsonic_url, subsonicUsername: s.subsonic_username, hasSubsonicPassword: !!s.subsonic_password, @@ -36,15 +41,20 @@ export function subsonicConfigComplete(s: SettingsRow): boolean { export async function registerSettingsRoutes(app: FastifyInstance): Promise { app.get('/api/settings', { preHandler: [requireAuth] }, async (request) => { - const s = getSettings(request.server.db, (request.user as any).id) + const s = getSettings(request.server.db, (request.user as UserRow).id) return settingsView(s) }) app.put('/api/settings', { preHandler: [requireAuth] }, async (request, reply) => { const db = request.server.db - const userId = (request.user as any).id + const userId = (request.user as UserRow).id const s = getSettings(db, userId) const body = (request.body ?? {}) as Record + for (const value of Object.values(body)) { + if (value !== undefined && typeof value !== 'string') { + return reply.code(400).send({ error: 'invalid_input', detail: 'settings fields must be strings' }) + } + } const next = { discogs_token: body.discogsToken !== undefined ? body.discogsToken || null : s.discogs_token, diff --git a/server/src/subsonic.ts b/server/src/subsonic.ts index cf63f61..7b89500 100644 --- a/server/src/subsonic.ts +++ b/server/src/subsonic.ts @@ -45,11 +45,11 @@ export class SubsonicClient { } private async request(endpoint: string, params: Record = {}): Promise { - const url = new URL(`${this.base}/rest/${endpoint}`) - const search = { ...this.authParams(), ...params } - for (const [k, v] of Object.entries(search)) url.searchParams.set(k, v) let res: Response try { + const url = new URL(`${this.base}/rest/${endpoint}`) + const search = { ...this.authParams(), ...params } + for (const [k, v] of Object.entries(search)) url.searchParams.set(k, v) res = await this.fetchImpl(url.toString()) } catch { throw new SubsonicError('unreachable', `could not reach ${this.base}`)