From aa6252d79fdf0474564ee0a770c8b8dad443fc73 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:39:06 -0800 Subject: [PATCH 1/8] feat: add data URL validation --- api/src/app/utils/validation.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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; -- 2.52.0 From b3eac4f584179c8902bd08e3e7b14d6ae60cdf6f Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:40:46 -0800 Subject: [PATCH 2/8] feat: validate book image correctly --- api/src/app/services/book.service.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index 2980cb2..d279f2c 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.IMAGE_DATA) { + 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 -- 2.52.0 From 7e47e87d85c8b6cfed92dfeec347c20661bd3fdc Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:43:59 -0800 Subject: [PATCH 3/8] fix: correct enum value in books --- api/src/app/services/book.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index d279f2c..638d302 100644 --- a/api/src/app/services/book.service.ts +++ b/api/src/app/services/book.service.ts @@ -48,7 +48,7 @@ export class BookService { if (data.coverImage) { if (data.coverImage.startsWith("data:")) { const sizeInBytes = data.coverImage.length * 0.75; - if (sizeInBytes > MAX_LENGTHS.IMAGE_DATA) { + if (sizeInBytes > MAX_LENGTHS.DATA_URL) { throw new Error("Cover image must be under 5MB."); } if (!validateDataUrl(data.coverImage)) { -- 2.52.0 From 4b2a8e6fed6f07197f3983a2c08f6d91cbdcdf18 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:46:18 -0800 Subject: [PATCH 4/8] fix: handle base64 in game service --- api/src/app/services/game.service.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 -- 2.52.0 From 08e25d9f606228019f7d59e2ae625b469f4253e2 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:46:59 -0800 Subject: [PATCH 5/8] fix: handle base64 in manga service --- api/src/app/services/manga.service.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 -- 2.52.0 From 2e7e718def06e5abfeb6fcc73cf5d55dde0977bc Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:47:29 -0800 Subject: [PATCH 6/8] fix: handle base64 in music service --- api/src/app/services/music.service.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index 34890f6..de53cc5 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.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 -- 2.52.0 From f2d357654720f3bdb37b431c2d13c8560f917ca2 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:48:44 -0800 Subject: [PATCH 7/8] fix: handle base64 in show service --- api/src/app/services/show.service.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 -- 2.52.0 From e3a846ac3b55557818299bbd8ad01830fa0474a0 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 20 Feb 2026 16:52:12 -0800 Subject: [PATCH 8/8] fix: correct prop for music --- api/src/app/services/music.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index de53cc5..c0480d8 100644 --- a/api/src/app/services/music.service.ts +++ b/api/src/app/services/music.service.ts @@ -43,20 +43,20 @@ export class MusicService { } // Validate cover art URL - if (data.coverImage) { - if (data.coverImage.startsWith("data:")) { - const sizeInBytes = data.coverImage.length * 0.75; + 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.coverImage)) { + if (!validateDataUrl(data.coverArt)) { throw new Error("Invalid image data URL."); } } else { - if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) { + if (!validateStringLength(data.coverArt, MAX_LENGTHS.URL)) { throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); } - if (!validateUrl(data.coverImage)) { + if (!validateUrl(data.coverArt)) { throw new Error("Invalid cover image URL. Only http and https URLs are allowed."); } } -- 2.52.0