diff --git a/api/package.json b/api/package.json index 9fc2bb960b1..a180f202a75 100644 --- a/api/package.json +++ b/api/package.json @@ -23,6 +23,7 @@ "@fastify/type-provider-typebox": "3.2.0", "@types/express-session": "1.17.7", "@types/supertest": "2.0.12", + "ajv": "8.12.0", "dotenv-cli": "7.2.1", "jest": "29.5.0", "pino-pretty": "10.0.0", diff --git a/api/src/routes/deprecated-endpoints.ts b/api/src/routes/deprecated-endpoints.ts index 51d73270c37..7cb08194d9a 100644 --- a/api/src/routes/deprecated-endpoints.ts +++ b/api/src/routes/deprecated-endpoints.ts @@ -1,7 +1,5 @@ -import { - FastifyPluginCallbackTypebox, - Type -} from '@fastify/type-provider-typebox'; +import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'; +import { schemas } from '../schemas'; type Endpoints = [string, 'GET' | 'POST'][]; @@ -20,18 +18,7 @@ export const deprecatedEndpoints: FastifyPluginCallbackTypebox = ( fastify.route({ method, url: endpoint, - schema: { - response: { - 410: Type.Object({ - message: Type.Object({ - type: Type.Literal('info'), - message: Type.Literal( - 'Please reload the app, this feature is no longer available.' - ) - }) - }) - } - }, + schema: schemas.deprecatedEndpoints, handler: async (_req, reply) => { void reply.status(410); return { diff --git a/api/src/routes/settings.ts b/api/src/routes/settings.ts index 9a5f9a3be92..b2ce3576864 100644 --- a/api/src/routes/settings.ts +++ b/api/src/routes/settings.ts @@ -1,7 +1,6 @@ -import { - Type, - type FastifyPluginCallbackTypebox -} from '@fastify/type-provider-typebox'; +import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'; + +import { schemas } from '../schemas'; export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify, @@ -17,32 +16,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify.put( '/update-my-profileui', { - schema: { - body: Type.Object({ - profileUI: Type.Object({ - isLocked: Type.Boolean(), - showAbout: Type.Boolean(), - showCerts: Type.Boolean(), - showDonation: Type.Boolean(), - showHeatMap: Type.Boolean(), - showLocation: Type.Boolean(), - showName: Type.Boolean(), - showPoints: Type.Boolean(), - showPortfolio: Type.Boolean(), - showTimeLine: Type.Boolean() - }) - }), - response: { - 200: Type.Object({ - message: Type.Literal('flash.privacy-updated'), - type: Type.Literal('success') - }), - 500: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) - } - } + schema: schemas.updateMyProfileUI }, async (req, reply) => { try { @@ -80,21 +54,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify.put( '/update-my-theme', { - schema: { - body: Type.Object({ - theme: Type.Union([Type.Literal('default'), Type.Literal('night')]) - }), - response: { - 200: Type.Object({ - message: Type.Literal('flash.updated-themes'), - type: Type.Literal('success') - }), - 500: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) - } - } + schema: schemas.updateMyTheme }, async (req, reply) => { try { @@ -166,21 +126,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify.put( '/update-my-keyboard-shortcuts', { - schema: { - body: Type.Object({ - keyboardShortcuts: Type.Boolean() - }), - response: { - 200: Type.Object({ - message: Type.Literal('flash.keyboard-shortcut-updated'), - type: Type.Literal('success') - }), - 500: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) - } - } + schema: schemas.updateMyKeyboardShortcuts }, async (req, reply) => { try { @@ -206,21 +152,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify.put( '/update-my-quincy-email', { - schema: { - body: Type.Object({ - sendQuincyEmail: Type.Boolean() - }), - response: { - 200: Type.Object({ - message: Type.Literal('flash.subscribe-to-quincy-updated'), - type: Type.Literal('success') - }), - 500: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) - } - } + schema: schemas.updateMyQuincyEmail }, async (req, reply) => { try { @@ -246,21 +178,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify.put( '/update-my-honesty', { - schema: { - body: Type.Object({ - isHonest: Type.Literal(true) - }), - response: { - 200: Type.Object({ - message: Type.Literal('buttons.accepted-honesty'), - type: Type.Literal('success') - }), - 500: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) - } - } + schema: schemas.updateMyHonesty }, async (req, reply) => { try { @@ -286,21 +204,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = ( fastify.put( '/update-privacy-terms', { - schema: { - body: Type.Object({ - quincyEmails: Type.Boolean() - }), - response: { - 200: Type.Object({ - message: Type.Literal('flash.privacy-updated'), - type: Type.Literal('success') - }), - 500: Type.Object({ - message: Type.Literal('flash.wrong-updating'), - type: Type.Literal('danger') - }) - } - } + schema: schemas.updateMyPrivacyTerms }, async (req, reply) => { try { diff --git a/api/src/schema.test.ts b/api/src/schema.test.ts new file mode 100644 index 00000000000..fcbc7e5ffb5 --- /dev/null +++ b/api/src/schema.test.ts @@ -0,0 +1,44 @@ +import Ajv from 'ajv'; +import secureSchema from 'ajv/lib/refs/json-schema-secure.json'; + +import { schemas } from './schemas'; + +// it's not strict, but that's okay - we're not using it to validate data +const ajv = new Ajv({ strictTypes: false }); +const isSchemaSecure = ajv.compile(secureSchema); + +describe('Schemas do not use obviously dangerous validation', () => { + Object.entries(schemas).forEach(([name, schema]) => { + describe(`schema ${name} is okay`, () => { + if ('body' in schema) { + test('body is secure', () => { + expect(isSchemaSecure(schema.body)).toBeTruthy(); + }); + } + + if ('querystring' in schema) { + test('querystring is secure', () => { + expect(isSchemaSecure(schema.querystring)).toBeTruthy(); + }); + } + + if ('params' in schema) { + test('params is secure', () => { + expect(isSchemaSecure(schema.params)).toBeTruthy(); + }); + } + + if ('headers' in schema) { + test('headers is secure', () => { + expect(isSchemaSecure(schema.headers)).toBeTruthy(); + }); + } + + Object.entries(schema.response).forEach(([code, codeSchema]) => { + test(`response ${code} is secure`, () => { + expect(isSchemaSecure(codeSchema)).toBeTruthy(); + }); + }); + }); + }); +}); diff --git a/api/src/schemas.ts b/api/src/schemas.ts new file mode 100644 index 00000000000..58b7c42789e --- /dev/null +++ b/api/src/schemas.ts @@ -0,0 +1,119 @@ +import { Type } from '@fastify/type-provider-typebox'; + +export const schemas = { + // Settings: + updateMyProfileUI: { + body: Type.Object({ + profileUI: Type.Object({ + isLocked: Type.Boolean(), + showAbout: Type.Boolean(), + showCerts: Type.Boolean(), + showDonation: Type.Boolean(), + showHeatMap: Type.Boolean(), + showLocation: Type.Boolean(), + showName: Type.Boolean(), + showPoints: Type.Boolean(), + showPortfolio: Type.Boolean(), + showTimeLine: Type.Boolean() + }) + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.privacy-updated'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, + updateMyTheme: { + body: Type.Object({ + theme: Type.Union([Type.Literal('default'), Type.Literal('night')]) + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.updated-themes'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, + updateMyKeyboardShortcuts: { + body: Type.Object({ + keyboardShortcuts: Type.Boolean() + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.keyboard-shortcut-updated'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, + updateMyQuincyEmail: { + body: Type.Object({ + sendQuincyEmail: Type.Boolean() + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.subscribe-to-quincy-updated'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, + updateMyHonesty: { + body: Type.Object({ + isHonest: Type.Literal(true) + }), + response: { + 200: Type.Object({ + message: Type.Literal('buttons.accepted-honesty'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, + updateMyPrivacyTerms: { + body: Type.Object({ + quincyEmails: Type.Boolean() + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.privacy-updated'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, + // Deprecated endpoints: + deprecatedEndpoints: { + response: { + 410: Type.Object({ + message: Type.Object({ + type: Type.Literal('info'), + message: Type.Literal( + 'Please reload the app, this feature is no longer available.' + ) + }) + }) + } + } +}; diff --git a/api/src/schemas/example.ts b/api/src/schemas/example.ts deleted file mode 100644 index 5df244840ff..00000000000 --- a/api/src/schemas/example.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Static, Type } from '@fastify/type-provider-typebox'; - -// The schema that TypeBox generates is compatible with ajv, e.g. the -// Type.Object call below puts the following object into subSchema. -/* -{ - type: 'object', - properties: { - bat: { type: 'number' }, - baz: { type: 'string' } - }, - required: ['bat', 'baz'] -} - */ - -export const subSchema = Type.Object({ - bat: Type.Integer(), - baz: Type.String() -}); - -export const responseSchema = Type.Object({ - value: Type.String(), - otherValue: Type.Boolean(), - optional: Type.Optional(Type.String()) -}); - -// The schema types would be the only code the client needs to import -// { value: string; otherValue: boolean; optional?: string | undefined;} -export type ResponseSchema = Static; -// { bat: number; baz: string; } -export type SubSchema = Static; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c934b3b98d6..0df37d938ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: '@types/supertest': specifier: 2.0.12 version: 2.0.12 + ajv: + specifier: 8.12.0 + version: 8.12.0 dotenv-cli: specifier: 7.2.1 version: 7.2.1 @@ -4056,7 +4059,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.10.5 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.12.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4064,7 +4067,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.20.2 dev: true /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.18.0): @@ -4082,7 +4085,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.8): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4090,7 +4093,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.18.0): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -4132,7 +4135,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.8): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -4140,7 +4143,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.18.0): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}