fix: mitigate SSRF vulnerabilities

This commit is contained in:
Pierre
2025-03-10 19:06:30 +01:00
committed by HiItsStolas
parent 3b4529f3b1
commit 0c3ecc718b
4 changed files with 148 additions and 36 deletions

View File

@@ -150,42 +150,84 @@ class CoverArtArchive extends ExternalAPI {
): Promise<Record<string, string | null>> {
if (!ids.length) return {};
const validIds = ids.filter(
(id) =>
typeof id === 'string' &&
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(
id
)
);
if (!validIds.length) return {};
const metadataRepository = getRepository(MetadataAlbum);
const existingMetadata = await metadataRepository.find({
where: { mbAlbumId: In(ids) },
where: { mbAlbumId: In(validIds) },
select: ['mbAlbumId', 'caaUrl', 'updatedAt'],
});
const metadataMap = new Map(
existingMetadata.map((metadata) => [metadata.mbAlbumId, metadata])
);
const results: Record<string, string | null> = {};
const idsToFetch: string[] = [];
ids.forEach((id) => {
const metadata = existingMetadata.find((m) => m.mbAlbumId === id);
for (const id of validIds) {
const metadata = metadataMap.get(id);
if (metadata?.caaUrl) {
results[id] = metadata.caaUrl;
Object.defineProperty(results, id, {
value: metadata.caaUrl,
enumerable: true,
writable: true,
});
} else if (metadata && !this.isMetadataStale(metadata)) {
results[id] = null;
Object.defineProperty(results, id, {
value: null,
enumerable: true,
writable: true,
});
} else {
idsToFetch.push(id);
}
});
}
if (idsToFetch.length > 0) {
const batchPromises = idsToFetch.map((id) =>
this.fetchCoverArt(id)
.then((response) => {
const frontImage = response.images.find((img) => img.front);
results[id] = frontImage?.thumbnails?.[250] || null;
return true;
})
.catch(() => {
results[id] = null;
return false;
})
);
try {
const batchPromises = idsToFetch.map((id) =>
this.fetchCoverArt(id)
.then((response) => {
const frontImage = response.images.find((img) => img.front);
Object.defineProperty(results, id, {
value: frontImage?.thumbnails?.[250] || null,
enumerable: true,
writable: true,
});
return true;
})
.catch((error) => {
logger.error('Failed to fetch cover art', {
label: 'CoverArtArchive',
id,
error: error instanceof Error ? error.message : 'Unknown error',
});
Object.defineProperty(results, id, {
value: null,
enumerable: true,
writable: true,
});
return false;
})
);
await Promise.all(batchPromises);
await Promise.all(batchPromises);
} catch (error) {
logger.error('Failed to process cover art requests', {
label: 'CoverArtArchive',
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return results;

View File

@@ -98,16 +98,27 @@ class MusicBrainz extends ExternalAPI {
artistMbid: string;
language?: string;
}): Promise<{ title: string; url: string; content: string } | null> {
if (
!artistMbid ||
typeof artistMbid !== 'string' ||
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(
artistMbid
)
) {
throw new Error('Invalid MusicBrainz artist ID format');
}
try {
const response = await fetch(
`https://musicbrainz.org/artist/${artistMbid}/wikipedia-extract`,
{
headers: {
Accept: 'application/json',
'Accept-Language': language,
},
}
);
const safeUrl = `https://musicbrainz.org/artist/${artistMbid}/wikipedia-extract`;
const response = await fetch(safeUrl, {
headers: {
Accept: 'application/json',
'Accept-Language': language,
'User-Agent':
'Jellyseerr/1.0.0 (https://github.com/Fallenbagel/jellyseerr)',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -115,7 +126,7 @@ class MusicBrainz extends ExternalAPI {
const data = await response.json();
if (!data.wikipediaExtract || !data.wikipediaExtract.content) {
throw new Error('No Wikipedia extract found');
return null;
}
const cleanContent = purify.sanitize(data.wikipediaExtract.content, {
@@ -129,7 +140,11 @@ class MusicBrainz extends ExternalAPI {
content: cleanContent.trim(),
};
} catch (error) {
throw new Error(`Error fetching Wikipedia extract: ${error.message}`);
throw new Error(
`[MusicBrainz] Failed to fetch Wikipedia extract: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}

View File

@@ -360,6 +360,30 @@ const ArtistDetails = () => {
async (albumType: string): Promise<void> => {
if (!artistId) return;
if (
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(
artistId
)
) {
return;
}
const validAlbumTypes = [
'Album',
'EP',
'Single',
'Live',
'Compilation',
'Remix',
'Soundtrack',
'Broadcast',
'Demo',
'Other',
];
if (!validAlbumTypes.includes(albumType)) {
return;
}
setAlbumTypes((prev) => ({
...prev,
[albumType]: {
@@ -369,10 +393,11 @@ const ArtistDetails = () => {
}));
try {
const pageSize = Math.min(data?.typeCounts?.[albumType] || 100, 1000);
const response = await fetch(
`/api/v1/artist/${artistId}?albumType=${albumType}&pageSize=${
data?.typeCounts?.[albumType] || 100
}`
`/api/v1/artist/${artistId}?albumType=${encodeURIComponent(
albumType
)}&pageSize=${pageSize}`
);
if (response.ok) {

View File

@@ -392,6 +392,31 @@ const PersonDetails = () => {
async (albumType: string): Promise<void> => {
if (!personId) return;
const parsedPersonId = parseInt(personId, 10);
if (
isNaN(parsedPersonId) ||
parsedPersonId <= 0 ||
parsedPersonId.toString() !== personId
) {
return;
}
const validAlbumTypes = [
'Album',
'EP',
'Single',
'Live',
'Compilation',
'Remix',
'Soundtrack',
'Broadcast',
'Demo',
'Other',
];
if (!validAlbumTypes.includes(albumType)) {
return;
}
setAlbumTypes((prev) => ({
...prev,
[albumType]: {
@@ -401,9 +426,14 @@ const PersonDetails = () => {
}));
try {
const pageSize = data?.artist?.typeCounts?.[albumType] || 100;
const pageSize = Math.min(
data?.artist?.typeCounts?.[albumType] || 100,
1000
);
const response = await fetch(
`/api/v1/person/${personId}?albumType=${albumType}&pageSize=${pageSize}`
`/api/v1/person/${parsedPersonId}?albumType=${encodeURIComponent(
albumType
)}&pageSize=${pageSize}`
);
if (response.ok) {
@@ -434,7 +464,7 @@ const PersonDetails = () => {
},
}));
}
} catch {
} catch (error) {
setAlbumTypes((prev) => ({
...prev,
[albumType]: {