diff --git a/server/api/admin/events.post.js b/server/api/admin/events.post.js index 08a3543..c64332f 100644 --- a/server/api/admin/events.post.js +++ b/server/api/admin/events.post.js @@ -1,5 +1,4 @@ import Event from "../../models/event.js"; -import Tag from "../../models/tag.js"; import { connectDB } from "../../utils/mongoose.js"; import { requireAdmin } from "../../utils/auth.js"; import { validateBody } from "../../utils/validateBody.js"; @@ -13,18 +12,7 @@ export default defineEventHandler(async (event) => { await connectDB(); - // Validate tag slugs against Tag collection - if (body.tags && body.tags.length > 0) { - const foundTags = await Tag.find({ slug: { $in: body.tags } }); - const foundSlugs = new Set(foundTags.map((t) => t.slug)); - const invalid = body.tags.filter((s) => !foundSlugs.has(s)); - if (invalid.length > 0) { - throw createError({ - statusCode: 400, - statusMessage: `Unknown tag slugs: ${invalid.join(", ")}`, - }); - } - } + await validateTagSlugs(body.tags); const eventData = { ...body, diff --git a/server/api/admin/events/[id].put.js b/server/api/admin/events/[id].put.js index 60809a7..92c0945 100644 --- a/server/api/admin/events/[id].put.js +++ b/server/api/admin/events/[id].put.js @@ -1,5 +1,4 @@ import Event from '../../../models/event.js' -import Tag from '../../../models/tag.js' import { connectDB } from '../../../utils/mongoose.js' import { requireAdmin } from '../../../utils/auth.js' @@ -12,18 +11,7 @@ export default defineEventHandler(async (event) => { await connectDB() - // Validate tag slugs against Tag collection - if (body.tags && body.tags.length > 0) { - const foundTags = await Tag.find({ slug: { $in: body.tags } }) - const foundSlugs = new Set(foundTags.map(t => t.slug)) - const invalid = body.tags.filter(s => !foundSlugs.has(s)) - if (invalid.length > 0) { - throw createError({ - statusCode: 400, - statusMessage: `Unknown tag slugs: ${invalid.join(', ')}` - }) - } - } + await validateTagSlugs(body.tags) const updateData = { ...body, diff --git a/server/api/admin/wiki/[id].patch.js b/server/api/admin/wiki/[id].patch.js index 1ad9a08..0d9d88a 100644 --- a/server/api/admin/wiki/[id].patch.js +++ b/server/api/admin/wiki/[id].patch.js @@ -14,6 +14,8 @@ export default defineEventHandler(async (event) => { await connectDB() + await validateTagSlugs(body.tags) + const article = await WikiArticle.findByIdAndUpdate( id, { tags: body.tags }, diff --git a/server/api/admin/wiki/batch-tag.post.js b/server/api/admin/wiki/batch-tag.post.js index be0ebef..951a175 100644 --- a/server/api/admin/wiki/batch-tag.post.js +++ b/server/api/admin/wiki/batch-tag.post.js @@ -30,6 +30,8 @@ export default defineEventHandler(async (event) => { await connectDB() + await validateTagSlugs([...(body.addTags || []), ...(body.removeTags || [])]) + const filter = body.articleIds ? { _id: { $in: body.articleIds } } : { collection: body.collection } diff --git a/server/api/admin/wiki/index.get.js b/server/api/admin/wiki/index.get.js index b4baf5a..7b2a49b 100644 --- a/server/api/admin/wiki/index.get.js +++ b/server/api/admin/wiki/index.get.js @@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => { filter.collection = collection } if (search) { - filter.title = { $regex: search, $options: 'i' } + filter.title = { $regex: escapeRegex(search), $options: 'i' } } const articles = await WikiArticle.find(filter) diff --git a/server/api/members/directory.get.js b/server/api/members/directory.get.js index 8df00e6..7390679 100644 --- a/server/api/members/directory.get.js +++ b/server/api/members/directory.get.js @@ -42,8 +42,7 @@ export default defineEventHandler(async (event) => { } if (search) { - // Escape regex metacharacters to prevent ReDoS - const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escaped = escapeRegex(search); andConditions.push({ $or: [ { name: { $regex: escaped, $options: "i" } }, diff --git a/server/models/event.js b/server/models/event.js index 2fd5d6d..ff2e2b0 100644 --- a/server/models/event.js +++ b/server/models/event.js @@ -183,7 +183,6 @@ const eventSchema = new mongoose.Schema({ refundAmount: Number, }, ], - tags: [{ type: String }], createdBy: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, diff --git a/server/utils/escapeRegex.js b/server/utils/escapeRegex.js new file mode 100644 index 0000000..4013c44 --- /dev/null +++ b/server/utils/escapeRegex.js @@ -0,0 +1,3 @@ +export function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} diff --git a/server/utils/validateTagSlugs.js b/server/utils/validateTagSlugs.js new file mode 100644 index 0000000..5ada09d --- /dev/null +++ b/server/utils/validateTagSlugs.js @@ -0,0 +1,14 @@ +import Tag from '../models/tag.js' + +export async function validateTagSlugs(slugs) { + if (!slugs?.length) return + const foundTags = await Tag.find({ slug: { $in: slugs } }) + const foundSlugs = new Set(foundTags.map(t => t.slug)) + const invalid = slugs.filter(s => !foundSlugs.has(s)) + if (invalid.length > 0) { + throw createError({ + statusCode: 400, + statusMessage: `Unknown tag slugs: ${invalid.join(', ')}` + }) + } +} diff --git a/tests/server/api/event-tags.test.js b/tests/server/api/event-tags.test.js index 37a0eb9..6ef3bb9 100644 --- a/tests/server/api/event-tags.test.js +++ b/tests/server/api/event-tags.test.js @@ -29,6 +29,7 @@ vi.mock('../../../server/utils/schemas.js', () => ({ import Tag from '../../../server/models/tag.js' import { validateBody } from '../../../server/utils/validateBody.js' +import { validateTagSlugs } from '../../../server/utils/validateTagSlugs.js' import createHandler from '../../../server/api/admin/events.post.js' import updateHandler from '../../../server/api/admin/events/[id].put.js' import { createMockEvent } from '../helpers/createMockEvent.js' @@ -51,8 +52,9 @@ const validEventBody = { describe('Event tag validation', () => { beforeEach(() => { vi.clearAllMocks() - // Override the global validateBody stub (from setup.js) to use our mock + // Override global stubs (from setup.js) to use the real/mocked implementations globalThis.validateBody = validateBody + globalThis.validateTagSlugs = validateTagSlugs }) describe('create route (events.post)', () => { diff --git a/tests/server/setup.js b/tests/server/setup.js index 50fc8ef..f2fc815 100644 --- a/tests/server/setup.js +++ b/tests/server/setup.js @@ -41,3 +41,8 @@ vi.stubGlobal('requireAuth', vi.fn()) vi.stubGlobal('requireAdmin', vi.fn()) vi.stubGlobal('validateBody', vi.fn(async (event) => readBody(event))) vi.stubGlobal('logActivity', vi.fn()) +vi.stubGlobal('validateTagSlugs', vi.fn()) + +// Real server/utils that are safe to use as-is in tests +import { escapeRegex } from '../../server/utils/escapeRegex.js' +vi.stubGlobal('escapeRegex', escapeRegex)