diff --git a/server/api/opensearch/suggestions.get.ts b/server/api/opensearch/suggestions.get.ts index 335728405d..4c7146e581 100644 --- a/server/api/opensearch/suggestions.get.ts +++ b/server/api/opensearch/suggestions.get.ts @@ -1,27 +1,37 @@ -import type { NpmSearchResponse } from '#shared/types' -import { NPM_REGISTRY } from '#shared/utils/constants' +import * as v from 'valibot' +import { SearchQuerySchema } from '#shared/schemas/package' +import { CACHE_MAX_AGE_ONE_MINUTE, NPM_REGISTRY } from '#shared/utils/constants' export default defineCachedEventHandler( async event => { const query = getQuery(event) - const q = String(query.q || '').trim() - if (!q) { - return [q, []] - } + try { + const q = v.parse(SearchQuerySchema, query.q) + + if (!q) { + return [q, []] + } - const params = new URLSearchParams({ text: q, size: '10' }) - const response = await $fetch(`${NPM_REGISTRY}/-/v1/search?${params}`) + const params = new URLSearchParams({ text: q, size: '10' }) + const response = await $fetch(`${NPM_REGISTRY}/-/v1/search?${params}`) - const suggestions = response.objects.map(obj => obj.package.name) - return [q, suggestions] + const suggestions = response.objects.map(obj => obj.package.name) + return [q, suggestions] + } catch (error: unknown) { + handleApiError(error, { + statusCode: 502, + message: ERROR_SUGGESTIONS_FETCH_FAILED, + }) + } }, { - maxAge: 60, + maxAge: CACHE_MAX_AGE_ONE_MINUTE, swr: true, getKey: event => { const query = getQuery(event) - return `opensearch-suggestions:${query.q || ''}` + const q = String(query.q || '').trim() + return `opensearch-suggestions:${q}` }, }, ) diff --git a/shared/schemas/package.ts b/shared/schemas/package.ts index d4d9fc730b..386d161066 100644 --- a/shared/schemas/package.ts +++ b/shared/schemas/package.ts @@ -35,6 +35,15 @@ export const FilePathSchema = v.pipe( v.check(input => !input.startsWith('/'), 'Invalid path: must be relative to package root'), ) +/** + * Schema for search queries, limits length to guard against DoS attacks + */ +export const SearchQuerySchema = v.pipe( + v.string(), + v.trim(), + v.maxLength(100, 'Search query is too long'), +) + /** * Schema for package fetching where version is not required */ diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts index 5ec5f49200..e4efd422f6 100644 --- a/shared/utils/constants.ts +++ b/shared/utils/constants.ts @@ -1,4 +1,5 @@ // Duration +export const CACHE_MAX_AGE_ONE_MINUTE = 60 export const CACHE_MAX_AGE_ONE_HOUR = 60 * 60 export const CACHE_MAX_AGE_ONE_DAY = 60 * 60 * 24 export const CACHE_MAX_AGE_ONE_YEAR = 60 * 60 * 24 * 365 @@ -14,3 +15,4 @@ export const ERROR_CALC_INSTALL_SIZE_FAILED = 'Failed to calculate install size. export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!' export const ERROR_JSR_FETCH_FAILED = 'Failed to fetch package from JSR registry.' export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.' +export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.'