diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index 2980cb2..638d302 100644 --- a/api/src/app/services/book.service.ts +++ b/api/src/app/services/book.service.ts @@ -10,6 +10,7 @@ import { validateUrl, validateRating, validateStringLength, + validateDataUrl, MAX_LENGTHS, } from "../utils/validation"; @@ -44,9 +45,23 @@ export class BookService { throw new Error("Rating must be an integer between 0 and 10."); } - // Validate cover image URL - if (data.coverImage && !validateUrl(data.coverImage)) { - throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + if (data.coverImage) { + if (data.coverImage.startsWith("data:")) { + const sizeInBytes = data.coverImage.length * 0.75; + if (sizeInBytes > MAX_LENGTHS.DATA_URL) { + throw new Error("Cover image must be under 5MB."); + } + if (!validateDataUrl(data.coverImage)) { + throw new Error("Invalid image data URL."); + } + } else { + if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) { + throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); + } + if (!validateUrl(data.coverImage)) { + throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + } + } } // Validate tags diff --git a/api/src/app/services/game.service.ts b/api/src/app/services/game.service.ts index 040462a..bc15287 100644 --- a/api/src/app/services/game.service.ts +++ b/api/src/app/services/game.service.ts @@ -10,6 +10,7 @@ import { validateUrl, validateRating, validateStringLength, + validateDataUrl, MAX_LENGTHS, } from "../utils/validation"; @@ -42,8 +43,23 @@ export class GameService { } // Validate cover image URL - if (data.coverImage && !validateUrl(data.coverImage)) { - throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + if (data.coverImage) { + if (data.coverImage.startsWith("data:")) { + const sizeInBytes = data.coverImage.length * 0.75; + if (sizeInBytes > MAX_LENGTHS.DATA_URL) { + throw new Error("Cover image must be under 5MB."); + } + if (!validateDataUrl(data.coverImage)) { + throw new Error("Invalid image data URL."); + } + } else { + if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) { + throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); + } + if (!validateUrl(data.coverImage)) { + throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + } + } } // Validate tags diff --git a/api/src/app/services/manga.service.ts b/api/src/app/services/manga.service.ts index e164622..1fa19b5 100644 --- a/api/src/app/services/manga.service.ts +++ b/api/src/app/services/manga.service.ts @@ -10,6 +10,7 @@ import { validateUrl, validateRating, validateStringLength, + validateDataUrl, MAX_LENGTHS, } from "../utils/validation"; @@ -42,8 +43,23 @@ export class MangaService { } // Validate cover image URL - if (data.coverImage && !validateUrl(data.coverImage)) { - throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + if (data.coverImage) { + if (data.coverImage.startsWith("data:")) { + const sizeInBytes = data.coverImage.length * 0.75; + if (sizeInBytes > MAX_LENGTHS.DATA_URL) { + throw new Error("Cover image must be under 5MB."); + } + if (!validateDataUrl(data.coverImage)) { + throw new Error("Invalid image data URL."); + } + } else { + if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) { + throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); + } + if (!validateUrl(data.coverImage)) { + throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + } + } } // Validate tags diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index 34890f6..c0480d8 100644 --- a/api/src/app/services/music.service.ts +++ b/api/src/app/services/music.service.ts @@ -10,6 +10,7 @@ import { validateUrl, validateRating, validateStringLength, + validateDataUrl, MAX_LENGTHS, } from "../utils/validation"; @@ -42,8 +43,23 @@ export class MusicService { } // Validate cover art URL - if (data.coverArt && !validateUrl(data.coverArt)) { - throw new Error("Invalid cover art URL. Only http and https URLs are allowed."); + if (data.coverArt) { + if (data.coverArt.startsWith("data:")) { + const sizeInBytes = data.coverArt.length * 0.75; + if (sizeInBytes > MAX_LENGTHS.DATA_URL) { + throw new Error("Cover image must be under 5MB."); + } + if (!validateDataUrl(data.coverArt)) { + throw new Error("Invalid image data URL."); + } + } else { + if (!validateStringLength(data.coverArt, MAX_LENGTHS.URL)) { + throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); + } + if (!validateUrl(data.coverArt)) { + throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + } + } } // Validate tags diff --git a/api/src/app/services/show.service.ts b/api/src/app/services/show.service.ts index 51bcd40..cda0ec9 100644 --- a/api/src/app/services/show.service.ts +++ b/api/src/app/services/show.service.ts @@ -10,6 +10,7 @@ import { validateUrl, validateRating, validateStringLength, + validateDataUrl, MAX_LENGTHS, } from "../utils/validation"; @@ -39,8 +40,23 @@ export class ShowService { } // Validate cover image URL - if (data.coverImage && !validateUrl(data.coverImage)) { - throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + if (data.coverImage) { + if (data.coverImage.startsWith("data:")) { + const sizeInBytes = data.coverImage.length * 0.75; + if (sizeInBytes > MAX_LENGTHS.DATA_URL) { + throw new Error("Cover image must be under 5MB."); + } + if (!validateDataUrl(data.coverImage)) { + throw new Error("Invalid image data URL."); + } + } else { + if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) { + throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); + } + if (!validateUrl(data.coverImage)) { + throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); + } + } } // Validate tags diff --git a/api/src/app/utils/validation.ts b/api/src/app/utils/validation.ts index 08fdfa3..05792b4 100644 --- a/api/src/app/utils/validation.ts +++ b/api/src/app/utils/validation.ts @@ -4,6 +4,13 @@ * @author Naomi Carrigan */ +/** + * Validates that a URL is a proper base64 data string. + */ +export function validateDataUrl(url: string): boolean { + return /^data:image\/(jpeg|png|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(url); +} + /** * Validates that a URL is safe and points to an allowed protocol. * Prevents javascript:, data:, vbscript:, and file: URLs. @@ -83,4 +90,5 @@ export const MAX_LENGTHS = { NOTES: 5000, TAGS: 50, // per tag ISBN: 50, + DATA_URL: 5 * 1024 * 1024, // 5MB in bytes (not chars) } as const;