Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api_groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const apiGroups: FastifyPluginCallback<ApiGroupsOptions> = (
'/preset/:id',
{
schema: {
params: Type.Object({ id: Type.String() }),
params: Type.Object({ id: Type.String({ maxLength: 128 }) }),
response: {
200: Preset,
404: ErrorResponse
Expand All @@ -99,7 +99,7 @@ const apiGroups: FastifyPluginCallback<ApiGroupsOptions> = (
'/preset/:id',
{
schema: {
params: Type.Object({ id: Type.String() }),
params: Type.Object({ id: Type.String({ maxLength: 128 }) }),
body: UpdatePreset,
response: {
200: Preset,
Expand Down Expand Up @@ -143,7 +143,7 @@ const apiGroups: FastifyPluginCallback<ApiGroupsOptions> = (
'/preset/:id',
{
schema: {
params: Type.Object({ id: Type.String() }),
params: Type.Object({ id: Type.String({ maxLength: 128 }) }),
response: {
204: Type.Null(),
404: ErrorResponse
Expand Down
18 changes: 15 additions & 3 deletions src/api_ingests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ const apiIngests: FastifyPluginCallback<ApiIngestsOptions> = (
schema: {
description: 'Retrieves an ingest.',
params: Type.Object({
ingestId: Type.String({ minLength: 1, pattern: '^[0-9]+$' })
ingestId: Type.String({
minLength: 1,
maxLength: 128,
pattern: '^[0-9]+$'
})
}),
response: {
200: Ingest,
Expand Down Expand Up @@ -179,7 +183,11 @@ const apiIngests: FastifyPluginCallback<ApiIngestsOptions> = (
description:
'Modify an existing Ingest. By changing the label, the deviceOutput or the deviceInput, the ingest is updated and the new ingest is returned.',
params: Type.Object({
ingestId: Type.String({ minLength: 1, pattern: '^[0-9]+$' })
ingestId: Type.String({
minLength: 1,
maxLength: 128,
pattern: '^[0-9]+$'
})
}),
body: PatchIngest,
response: {
Expand Down Expand Up @@ -254,7 +262,11 @@ const apiIngests: FastifyPluginCallback<ApiIngestsOptions> = (
schema: {
description: 'Deletes a Ingest.',
params: Type.Object({
ingestId: Type.String({ minLength: 1, pattern: '^[0-9]+$' })
ingestId: Type.String({
minLength: 1,
maxLength: 128,
pattern: '^[0-9]+$'
})
}),
response: {
200: Type.String(),
Expand Down
12 changes: 10 additions & 2 deletions src/api_productions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,19 @@ function sortParticipants(participants: UserResponse[]): UserResponse[] {
// ── Param schemas for route validation ──────────────────────────────────

const ProductionIdParams = Type.Object({
productionId: Type.String({ minLength: 1, pattern: '^[0-9]+$' })
productionId: Type.String({
minLength: 1,
maxLength: 128,
pattern: '^[0-9]+$'
})
});

const ProductionLineParams = Type.Object({
productionId: Type.String({ minLength: 1, pattern: '^[0-9]+$' }),
productionId: Type.String({
minLength: 1,
maxLength: 128,
pattern: '^[0-9]+$'
}),
lineId: Type.String({ minLength: 1, maxLength: 200 })
});

Expand Down
67 changes: 67 additions & 0 deletions src/api_validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,4 +563,71 @@ describe('Input Validation', () => {
expect(response.statusCode).toBe(501);
});
});

// ── maxLength constraints (defence-in-depth, #239) ─────────────
// Unbounded Type.String() schemas now carry maxLength so oversized
// payloads are rejected by AJV with 400 before reaching handlers.

describe('maxLength constraints (#239)', () => {
test('GET /production/:productionId rejects an oversized productionId', async () => {
const response = await server.inject({
method: 'GET',
url: `/api/v1/production/${'1'.repeat(129)}`
});
// Rejected before the handler: Fastify caps params at maxParamLength
// (default 100) → 414, and the schema maxLength (128) would otherwise
// yield 400. Either way the oversized value never reaches the handler.
expect([400, 414]).toContain(response.statusCode);
});

test('POST /session rejects productionId exceeding 128 chars', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/v1/session',
body: {
productionId: '1'.repeat(129),
lineId: 'lid-1',
username: 'user'
}
});
expect(response.statusCode).toBe(400);
});

test('POST /session accepts a valid productionId within bounds', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/v1/session',
body: { productionId: '1', lineId: 'lid-1', username: 'user' }
});
// Param/body are valid — the request proceeds past schema validation.
expect(response.statusCode).not.toBe(400);
});

test('POST /ingest rejects label exceeding 200 chars (400, not 501)', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/v1/ingest',
body: { label: 'x'.repeat(201), ipAddress: '127.0.0.1' }
});
expect(response.statusCode).toBe(400);
});

test('POST /ingest rejects ipAddress exceeding 128 chars (400, not 501)', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/v1/ingest',
body: { label: 'valid', ipAddress: 'x'.repeat(129) }
});
expect(response.statusCode).toBe(400);
});

test('POST /ingest with valid body passes validation (501, not 400)', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/v1/ingest',
body: { label: 'valid', ipAddress: '127.0.0.1' }
});
expect(response.statusCode).toBe(501);
});
});
});
35 changes: 21 additions & 14 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export const Conference = Type.Object({
});

export const Line = Type.Object({
name: Type.String(),
name: Type.String({ maxLength: 200 }),
id: Type.String(),
smbConferenceId: Type.String(),
programOutputLine: Type.Optional(Type.Boolean())
Expand All @@ -278,7 +278,7 @@ export const PatchLineResponse = Type.Omit(Line, ['smbConferenceId']);

export const Production = Type.Object({
_id: Type.Number(),
name: Type.String(),
name: Type.String({ maxLength: 200 }),
lines: Type.Array(Line)
});

Expand All @@ -305,7 +305,11 @@ export const DetailedProductionResponse = Type.Object({
});

export const NewSession = Type.Object({
productionId: Type.String({ minLength: 1, pattern: '^[0-9]+$' }),
productionId: Type.String({
minLength: 1,
maxLength: 128,
pattern: '^[0-9]+$'
}),
lineId: Type.String({ minLength: 1, maxLength: 200 }),
username: Type.String({ minLength: 1, maxLength: 200 })
});
Expand Down Expand Up @@ -346,8 +350,11 @@ export const ReAuthResponse = Type.Object({
export type ReAuthResponse = Static<typeof ReAuthResponse>;

// WHIP/WHEP endpoint request body schema
// SDP offers are large multi-line blobs; 65536 matches the SdpAnswer bound and
// leaves ample room for real offers while rejecting abusive oversized payloads.
export const WhipWhepRequest = Type.String({
description: 'WebRTC SDP offer'
description: 'WebRTC SDP offer',
maxLength: 65536
});

// WHIP/WHEP endpoint response schema
Expand All @@ -356,8 +363,8 @@ export const WhipWhepResponse = Type.String({
});

export const NewIngest = Type.Object({
label: Type.String(),
ipAddress: Type.String()
label: Type.String({ maxLength: 200 }),
ipAddress: Type.String({ maxLength: 128 })
});

export const Ingest = Type.Object({
Expand Down Expand Up @@ -386,29 +393,29 @@ export const IngestListResponse = Type.Object({
});

export const PatchIngest = Type.Union([
Type.Object({ label: Type.String() }),
Type.Object({ label: Type.String({ maxLength: 200 }) }),
Type.Object({
deviceOutput: Type.Object({
name: Type.String(),
label: Type.String()
name: Type.String({ maxLength: 200 }),
label: Type.String({ maxLength: 200 })
})
}),
Type.Object({
deviceInput: Type.Object({
name: Type.String(),
label: Type.String()
name: Type.String({ maxLength: 200 }),
label: Type.String({ maxLength: 200 })
})
})
]);

export const PatchIngestResponse = Type.Omit(Ingest, ['ipAddress']);

export const PresetCall = Type.Object({
productionId: Type.String({ minLength: 1 }),
lineId: Type.String({ minLength: 1 }),
productionId: Type.String({ minLength: 1, maxLength: 128 }),
lineId: Type.String({ minLength: 1, maxLength: 128 }),
lineUsedForProgramOutput: Type.Optional(Type.Boolean()),
isProgramUser: Type.Optional(Type.Boolean()),
lineName: Type.Optional(Type.String())
lineName: Type.Optional(Type.String({ maxLength: 200 }))
});

export const NewPreset = Type.Object({
Expand Down
Loading