mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
test(api): schema security (#50413)
* test: confirm all schemas pass basic validation * refactor: use tested schemas in routes * chore: move ajv to dev deps
This commit is contained in:
committed by
GitHub
parent
1fc0bccb6f
commit
39857b5aa4
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+9
-105
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.'
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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<typeof responseSchema>;
|
||||
// { bat: number; baz: string; }
|
||||
export type SubSchema = Static<typeof subSchema>;
|
||||
Generated
+9
-6
@@ -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==}
|
||||
|
||||
Reference in New Issue
Block a user