fix(services): restrict GET /uploads/messages/:file to authorized users

This commit is contained in:
Divlo 2022-01-06 18:13:13 +01:00
parent 03946f26e7
commit 97b1d04261
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
23 changed files with 1314 additions and 2534 deletions

View File

@ -1,6 +1,6 @@
{ {
"extends": ["standard-with-typescript", "eslint-config-prettier"], "extends": ["standard-with-typescript", "prettier"],
"plugins": ["unicorn", "eslint-plugin-prettier"], "plugins": ["unicorn", "import", "prettier"],
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
@ -10,6 +10,11 @@
}, },
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "error",
"import/order": [
"error",
{ "groups": ["builtin", "external", "internal"] }
],
"import/extensions": ["error", "always"],
"unicorn/prefer-node-protocol": "error", "unicorn/prefer-node-protocol": "error",
"unicorn/prevent-abbreviations": "error" "unicorn/prevent-abbreviations": "error"
} }

View File

@ -21,13 +21,27 @@ jobs:
- name: 'Install' - name: 'Install'
run: 'npm install' run: 'npm install'
- run: 'npm run lint:commit -- --to "${{ github.sha }}"' - name: 'lint:commit'
- run: 'npm run lint:editorconfig' run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- run: 'npm run lint:markdown'
- run: 'npm run lint:docker'
- run: 'npm run lint:typescript'
- name: 'dotenv-linter' - name: 'lint:editorconfig'
run: 'npm run lint:editorconfig'
- name: 'lint:markdown'
run: 'npm run lint:markdown'
- name: 'lint:typescript'
run: 'npm run lint:typescript'
- name: 'lint:prettier'
run: 'npm run lint:prettier'
- name: 'lint:dotenv'
uses: 'dotenv-linter/action-dotenv-linter@v2' uses: 'dotenv-linter/action-dotenv-linter@v2'
with: with:
github_token: ${{ secrets.github_token }} github_token: ${{ secrets.github_token }}
- name: 'lint:docker'
uses: 'hadolint/hadolint-action@v1.6.0'
with:
dockerfile: './Dockerfile'

View File

@ -6,6 +6,5 @@
"jest --findRelatedTests" "jest --findRelatedTests"
], ],
"*.{yml,json}": ["prettier --write"], "*.{yml,json}": ["prettier --write"],
"*.{md}": ["prettier --write", "markdownlint --dot --fix"], "*.{md}": ["prettier --write", "markdownlint --dot --fix"]
"./Dockerfile": ["dockerfilelint"]
} }

View File

@ -3,3 +3,4 @@ node_modules
coverage coverage
package.json package.json
package-lock.json package-lock.json
*.hbs

View File

@ -1,5 +1,6 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifierEnding": "js",
"prettier.configPath": ".prettierrc.json", "prettier.configPath": ".prettierrc.json",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {

View File

@ -1,22 +1,23 @@
FROM node:16.13.1 AS dependencies FROM node:16.13.1 AS dependencies
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./package*.json ./ COPY ./package*.json ./
RUN npm clean-install RUN npm install
FROM node:16.13.1 AS builder FROM node:16.13.1 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=dependencies /usr/src/app/node_modules ./node_modules COPY --from=dependencies /usr/src/app/node_modules ./node_modules
COPY ./ ./ COPY ./ ./
RUN npx prisma generate RUN npm run prisma:generate && npm run build
RUN npm run build
FROM node:16.13.1 AS runner FROM node:16.13.1 AS runner
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production ENV NODE_ENV=production
COPY --from=builder /usr/src/app/node_modules ./node_modules COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/start.sh ./start.sh
COPY --from=builder /usr/src/app/package.json ./package.json
COPY --from=builder /usr/src/app/email ./email COPY --from=builder /usr/src/app/email ./email
COPY --from=builder /usr/src/app/build ./build COPY --from=builder /usr/src/app/build ./build
COPY --from=builder /usr/src/app/prisma ./prisma COPY --from=builder /usr/src/app/prisma ./prisma
COPY --from=builder /usr/src/app/uploads ./uploads COPY --from=builder /usr/src/app/uploads ./uploads
USER node USER node
CMD npm run prisma:migrate:deploy && node build/index.js CMD ["./docker-start.sh"]

4
docker-start.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
npm run prisma:migrate:deploy
node build/index.js

3538
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,10 +17,10 @@
"dev": "concurrently -k -n \"TypeScript,Node\" -p \"[{name}]\" -c \"blue,green\" \"swc ./src --out-dir ./build --watch\" \"cross-env NODE_ENV=development nodemon -e js,json,yaml build/index.js\"", "dev": "concurrently -k -n \"TypeScript,Node\" -p \"[{name}]\" -c \"blue,green\" \"swc ./src --out-dir ./build --watch\" \"cross-env NODE_ENV=development nodemon -e js,json,yaml build/index.js\"",
"generate": "plop", "generate": "plop",
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:docker": "dockerfilelint './Dockerfile'",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'", "lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'", "lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
"lint:prettier": "prettier '.' --check",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "jest", "test": "jest",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
@ -52,14 +52,14 @@
"ms": "2.1.3", "ms": "2.1.3",
"nodemailer": "6.7.2", "nodemailer": "6.7.2",
"read-pkg": "5.2.0", "read-pkg": "5.2.0",
"socket.io": "4.4.0" "socket.io": "4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "16.0.1", "@commitlint/cli": "16.0.1",
"@commitlint/config-conventional": "16.0.0", "@commitlint/config-conventional": "16.0.0",
"@saithodev/semantic-release-backmerge": "2.1.0", "@saithodev/semantic-release-backmerge": "2.1.0",
"@swc/cli": "0.1.55", "@swc/cli": "0.1.55",
"@swc/core": "1.2.124", "@swc/core": "1.2.127",
"@swc/jest": "0.2.15", "@swc/jest": "0.2.15",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/busboy": "0.3.1", "@types/busboy": "0.3.1",
@ -68,26 +68,25 @@
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/jsonwebtoken": "8.5.6", "@types/jsonwebtoken": "8.5.6",
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
"@types/node": "17.0.5", "@types/node": "17.0.8",
"@types/nodemailer": "6.4.4", "@types/nodemailer": "6.4.4",
"@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/eslint-plugin": "4.33.0",
"concurrently": "6.5.1", "concurrently": "7.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"dockerfilelint": "1.8.0",
"editorconfig-checker": "4.0.2", "editorconfig-checker": "4.0.2",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.3.0",
"eslint-config-standard-with-typescript": "21.0.1", "eslint-config-standard-with-typescript": "21.0.1",
"eslint-plugin-import": "2.25.3", "eslint-plugin-import": "2.25.4",
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "4.0.0", "eslint-plugin-prettier": "4.0.0",
"eslint-plugin-promise": "5.1.1", "eslint-plugin-promise": "5.1.1",
"eslint-plugin-unicorn": "40.0.0", "eslint-plugin-unicorn": "40.0.0",
"husky": "7.0.4", "husky": "7.0.4",
"jest": "27.4.5", "jest": "27.4.7",
"jest-mock-extended": "2.0.4", "jest-mock-extended": "2.0.4",
"jest-ts-webcompat-resolver": "1.0.0", "jest-ts-webcompat-resolver": "1.0.0",
"lint-staged": "12.1.4", "lint-staged": "12.1.5",
"markdownlint-cli": "0.30.0", "markdownlint-cli": "0.30.0",
"nodemon": "2.0.15", "nodemon": "2.0.15",
"plop": "3.0.5", "plop": "3.0.5",

View File

@ -1,6 +1,5 @@
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset } from 'jest-mock-extended' import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import { DeepMockProxy } from 'jest-mock-extended/lib/cjs/Mock'
import prisma from '../tools/database/prisma.js' import prisma from '../tools/database/prisma.js'

View File

@ -6,8 +6,8 @@ import { userSettingsExample } from '../../models/UserSettings.js'
import { import {
generateAccessToken, generateAccessToken,
generateRefreshToken generateRefreshToken
} from '../../tools/utils/jwtToken' } from '../../tools/utils/jwtToken.js'
import { prismaMock } from '../setup' import { prismaMock } from '../setup.js'
export const authenticateUserTest = async (): Promise<{ export const authenticateUserTest = async (): Promise<{
accessToken: string accessToken: string

View File

@ -1,7 +1,7 @@
import { application } from '../../../../application.js' import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js' import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../../__test__/setup.js' import { prismaMock } from '../../../../__test__/setup.js'
import { memberExample } from '../../../../models/Member' import { memberExample } from '../../../../models/Member.js'
import { channelExample } from '../../../../models/Channel.js' import { channelExample } from '../../../../models/Channel.js'
describe('GET /channels/[channelId]', () => { describe('GET /channels/[channelId]', () => {

View File

@ -44,7 +44,6 @@ const postServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431],
500: fastifyErrors[500] 500: fastifyErrors[500]
} }
} as const } as const

View File

@ -42,6 +42,7 @@ const postServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431],
500: fastifyErrors[500] 500: fastifyErrors[500]
} }
} as const } as const

View File

@ -1,8 +1,8 @@
import { application } from '../../../../../application.js' import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js' import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../../../__test__/setup.js' import { prismaMock } from '../../../../../__test__/setup.js'
import { memberExample } from '../../../../../models/Member' import { memberExample } from '../../../../../models/Member.js'
import { guildExample } from '../../../../../models/Guild' import { guildExample } from '../../../../../models/Guild.js'
import { channelExample } from '../../../../../models/Channel.js' import { channelExample } from '../../../../../models/Channel.js'
describe('GET /guilds/[guildId]/channels', () => { describe('GET /guilds/[guildId]/channels', () => {

View File

@ -1,9 +1,9 @@
import { Static, Type } from '@sinclair/typebox' import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { FastifyPluginAsync, FastifySchema } from 'fastify'
import fastifyMultipart from 'fastify-multipart'
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../tools/database/prisma.js' import prisma from '../../../../tools/database/prisma.js'
import { uploadFile } from '../../../../tools/utils/uploadFile.js' import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import { guildSchema } from '../../../../models/Guild.js' import { guildSchema } from '../../../../models/Guild.js'

View File

@ -0,0 +1,40 @@
import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../../models/utils.js'
const parameters = Type.Object({
file: Type.String()
})
type Parameters = Static<typeof parameters>
export const getServiceSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parameters,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const getGuildsUploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{
Params: Parameters
}>({
method: 'GET',
url: '/uploads/guilds/:file',
schema: getServiceSchema,
handler: async (request, reply) => {
const { file } = request.params
return await reply.sendFile(path.join('guilds', file))
}
})
}

View File

@ -1,58 +1,11 @@
import path from 'node:path' import { FastifyPluginAsync } from 'fastify'
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { getGuildsUploadsService } from './guilds/get.js'
import { Static, Type } from '@sinclair/typebox' import { getMessagesUploadsService } from './messages/get.js'
import { getUsersUploadsService } from './users/get.js'
import { fastifyErrors } from '../../models/utils'
const parametersUploadsSchema = Type.Object({
image: Type.String()
})
type ParametersUploadsSchemaType = Static<typeof parametersUploadsSchema>
const getUploadsSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parametersUploadsSchema,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const uploadsService: FastifyPluginAsync = async (fastify) => { export const uploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ Params: ParametersUploadsSchemaType }>({ await fastify.register(getGuildsUploadsService)
method: 'GET', await fastify.register(getMessagesUploadsService)
url: '/uploads/users/:image', await fastify.register(getUsersUploadsService)
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('users', image))
}
})
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/guilds/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('guilds', image))
}
})
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/messages/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('messages', image))
}
})
} }

View File

@ -0,0 +1,76 @@
import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import prisma from '../../../tools/database/prisma.js'
const parameters = Type.Object({
file: Type.String()
})
type Parameters = Static<typeof parameters>
export const getServiceSchema: FastifySchema = {
tags: ['uploads'] as string[],
security: [
{
bearerAuth: []
}
] as Array<{ [key: string]: [] }>,
params: parameters,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const getMessagesUploadsService: FastifyPluginAsync = async (
fastify
) => {
await fastify.register(authenticateUser)
fastify.route<{
Params: Parameters
}>({
method: 'GET',
url: '/uploads/messages/:file',
schema: getServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const { file } = request.params
const message = await prisma.message.findFirst({
where: { value: `/uploads/messages/${file}` },
include: {
member: {
select: { guildId: true }
}
}
})
if (message == null) {
throw fastify.httpErrors.notFound('Message not found')
}
const member = await prisma.member.findFirst({
where: {
guildId: message.member?.guildId,
userId: request.user.current.id
}
})
if (member == null) {
throw fastify.httpErrors.notFound('Member not found')
}
return await reply.sendFile(path.join('messages', file))
}
})
}

View File

@ -0,0 +1,40 @@
import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../../models/utils.js'
const parameters = Type.Object({
file: Type.String()
})
type Parameters = Static<typeof parameters>
export const getServiceSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parameters,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const getUsersUploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{
Params: Parameters
}>({
method: 'GET',
url: '/uploads/users/:file',
schema: getServiceSchema,
handler: async (request, reply) => {
const { file } = request.params
return await reply.sendFile(path.join('users', file))
}
})
}

View File

@ -1,9 +1,9 @@
import { Type } from '@sinclair/typebox' import { Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { FastifyPluginAsync, FastifySchema } from 'fastify'
import fastifyMultipart from 'fastify-multipart'
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../tools/database/prisma.js' import prisma from '../../../../tools/database/prisma.js'
import { uploadFile } from '../../../../tools/utils/uploadFile.js' import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import { import {

View File

@ -1,6 +1,6 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import nodemailer from 'nodemailer' import nodemailer from 'nodemailer'
import SMTPTransport from 'nodemailer/lib/smtp-transport' import type SMTPTransport from 'nodemailer/lib/smtp-transport.js'
dotenv.config() dotenv.config()
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10) const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)

View File

@ -1,4 +1,4 @@
import { parseStringNullish } from '../parseStringNullish' import { parseStringNullish } from '../parseStringNullish.js'
const defaultString = 'defaultString' const defaultString = 'defaultString'