fix(SEC-API-19+20): Validate brain search length and limit params
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- Add @MaxLength(500) to BrainQueryDto.query and BrainQueryDto.search fields
- Create BrainSearchDto with validated q (max 500 chars) and limit (1-100) fields
- Update BrainController.search to use BrainSearchDto instead of raw query params
- Add defensive validation in BrainService.search and BrainService.query methods:
  - Reject search terms exceeding 500 characters with BadRequestException
  - Clamp limit to valid range [1, 100] for defense-in-depth
- Add comprehensive tests for DTO validation and service-level guards
- Update existing controller tests for new search method signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 13:29:03 -06:00
parent ef1f1eee9d
commit 17cfeb974b
6 changed files with 299 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
import { Injectable } from "@nestjs/common";
import { Injectable, BadRequestException } from "@nestjs/common";
import { EntityType, TaskStatus, ProjectStatus } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import type { BrainQueryDto, BrainContextDto, TaskFilter, EventFilter, ProjectFilter } from "./dto";
@@ -80,6 +80,11 @@ export interface BrainContext {
}[];
}
/** Maximum allowed length for search query strings */
const MAX_SEARCH_LENGTH = 500;
/** Maximum allowed limit for search results per entity type */
const MAX_SEARCH_LIMIT = 100;
/**
* @description Service for querying and aggregating workspace data for AI/brain operations.
* Provides unified access to tasks, events, and projects with filtering and search capabilities.
@@ -97,15 +102,28 @@ export class BrainService {
*/
async query(queryDto: BrainQueryDto): Promise<BrainQueryResult> {
const { workspaceId, entities, search, limit = 20 } = queryDto;
if (search && search.length > MAX_SEARCH_LENGTH) {
throw new BadRequestException(
`Search term must not exceed ${String(MAX_SEARCH_LENGTH)} characters`
);
}
if (queryDto.query && queryDto.query.length > MAX_SEARCH_LENGTH) {
throw new BadRequestException(
`Query must not exceed ${String(MAX_SEARCH_LENGTH)} characters`
);
}
const clampedLimit = Math.max(1, Math.min(limit, MAX_SEARCH_LIMIT));
const includeEntities = entities ?? [EntityType.TASK, EntityType.EVENT, EntityType.PROJECT];
const includeTasks = includeEntities.includes(EntityType.TASK);
const includeEvents = includeEntities.includes(EntityType.EVENT);
const includeProjects = includeEntities.includes(EntityType.PROJECT);
const [tasks, events, projects] = await Promise.all([
includeTasks ? this.queryTasks(workspaceId, queryDto.tasks, search, limit) : [],
includeEvents ? this.queryEvents(workspaceId, queryDto.events, search, limit) : [],
includeProjects ? this.queryProjects(workspaceId, queryDto.projects, search, limit) : [],
includeTasks ? this.queryTasks(workspaceId, queryDto.tasks, search, clampedLimit) : [],
includeEvents ? this.queryEvents(workspaceId, queryDto.events, search, clampedLimit) : [],
includeProjects
? this.queryProjects(workspaceId, queryDto.projects, search, clampedLimit)
: [],
]);
// Build filters object conditionally for exactOptionalPropertyTypes
@@ -259,10 +277,17 @@ export class BrainService {
* @throws PrismaClientKnownRequestError if database query fails
*/
async search(workspaceId: string, searchTerm: string, limit = 20): Promise<BrainQueryResult> {
if (searchTerm.length > MAX_SEARCH_LENGTH) {
throw new BadRequestException(
`Search term must not exceed ${String(MAX_SEARCH_LENGTH)} characters`
);
}
const clampedLimit = Math.max(1, Math.min(limit, MAX_SEARCH_LIMIT));
const [tasks, events, projects] = await Promise.all([
this.queryTasks(workspaceId, undefined, searchTerm, limit),
this.queryEvents(workspaceId, undefined, searchTerm, limit),
this.queryProjects(workspaceId, undefined, searchTerm, limit),
this.queryTasks(workspaceId, undefined, searchTerm, clampedLimit),
this.queryEvents(workspaceId, undefined, searchTerm, clampedLimit),
this.queryProjects(workspaceId, undefined, searchTerm, clampedLimit),
]);
return {