diff --git a/package-lock.json b/package-lock.json index 48fecd8..6a49228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "discord-api-types": "^0.37.100", "dotenv": "^16.4.5", "express": "^4.21.0", + "faithful-api": "file:", + "file-type": "^19.5.0", "firestorm-db": "^1.13.0", "form-data": "^4.0.0", "isomorphic-dompurify": "^2.15.0", @@ -726,6 +728,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -2940,6 +2952,10 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/faithful-api": { + "resolved": "", + "link": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3021,6 +3037,23 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.5.0.tgz", + "integrity": "sha512-dMuq6WWnP6BpQY0zYJNpTtQWgeCImSMG0BTIzUBXvxbwc1HWP/E7AE4UWU9XSCOPGJuOHda0HpDnwM2FW+d90A==", + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^8.1.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3333,6 +3366,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3682,6 +3730,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4019,6 +4086,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -4959,6 +5037,18 @@ "node": ">=8" } }, + "node_modules/peek-readable": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.2.0.tgz", + "integrity": "sha512-U94a+eXHzct7vAd19GH3UQ2dH4Satbng0MyYTMaQatL0pvYYL5CTPR25HBhKtecl+4bfu1/i3vC6k0hydO5Vcw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5767,6 +5857,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-8.1.0.tgz", + "integrity": "sha512-ExzDvHYPj6F6QkSNe/JxSlBxTh3OrI6wrAIz53ulxo1c4hBJ1bT9C/JrAthEKHWG9riVH3Xzg7B03Oxty6S2Lw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.1.4" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5876,6 +5982,22 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -6164,6 +6286,17 @@ "node": ">=0.8.0" } }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 7899872..0c3eccd 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "discord-api-types": "^0.37.100", "dotenv": "^16.4.5", "express": "^4.21.0", + "faithful-api": "file:", + "file-type": "^19.5.0", "firestorm-db": "^1.13.0", "form-data": "^4.0.0", "isomorphic-dompurify": "^2.15.0", diff --git a/src/v2/service/addon.service.ts b/src/v2/service/addon.service.ts index 5612b0e..7f756ac 100644 --- a/src/v2/service/addon.service.ts +++ b/src/v2/service/addon.service.ts @@ -1,6 +1,7 @@ import { URL } from "url"; import { APIEmbedField } from "discord-api-types/v10"; import { WriteConfirmation } from "firestorm-db"; +import { fileTypeFromBuffer, MimeType } from "file-type"; import { User, UserProfile } from "../interfaces/users"; import { Addons, Addon, AddonStatus, AddonAll, Files, File, FileParent } from "../interfaces"; import { BadRequestError, NotFoundError } from "../tools/errors"; @@ -17,6 +18,10 @@ import { import AddonFirestormRepository from "../repository/addon.repository"; import { discordEmbed } from "../tools/discordEmbed"; +const HEADER_MIME_TYPES: MimeType[] = ["image/jpeg"]; + +const SCREENSHOT_MIME_TYPES: MimeType[] = ["image/jpeg"]; + // filter & keep only values that are in a-Z & 0-9 & _ or - const toSlug = (value: string) => value @@ -31,6 +36,20 @@ export default class AddonService { private readonly addonRepo = new AddonFirestormRepository(); + /** + * Passes MIME type verification + * @param buffer Input file buffer + * @param mime_types_accepted List of accepted mime types + */ + private async verifyFileType(buffer: Buffer, mime_types_accepted: Array) { + const { mime } = await fileTypeFromBuffer(buffer); + if (!mime_types_accepted.includes(mime)) { + throw new BadRequestError( + `Incorrect file header, expected one in ${mime_types_accepted.toString()}, got ${mime}`, + ); + } + } + public async getIdFromPath(idOrSlug: string): Promise<[number, Addon | undefined]> { const intID = Number(idOrSlug); @@ -328,6 +347,8 @@ export default class AddonService { filename: string, buffer: Buffer, ): Promise { + this.verifyFileType(buffer, HEADER_MIME_TYPES); + const [addonID, addon] = await this.getAddonFromSlugOrId(idOrSlug); const { slug } = addon; @@ -373,6 +394,8 @@ export default class AddonService { filename: string, buffer: Buffer, ): Promise { + this.verifyFileType(buffer, SCREENSHOT_MIME_TYPES); + const [addonID, addon] = await this.getAddonFromSlugOrId(idOrSlug); const { slug } = addon;