From aaee70f6dfd2e08c27350f532a308ebabb24e0cd Mon Sep 17 00:00:00 2001 From: "oscargonzalezmoreno@gmail.com" Date: Sat, 28 Dec 2024 17:42:17 +0100 Subject: [PATCH] Upload size control and security improvements - Limit the maximum upload file size to 500 KB. - Minify CSS in the `userChrome` and `userContent` fields. - Sanitize uploaded JSON to remove XSS references (create and update). --- package-lock.json | 122 +++++++++++++++++++++++++++------- package.json | 5 +- src/rices/rices.controller.ts | 17 +++++ src/rices/rices.service.ts | 45 +++++++++++++ 4 files changed, 165 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 387d0e7..7dfef81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@nestjs/testing": "^10.0.0", "@nestjs/throttler": "^6.3.0", "@supabase/supabase-js": "^2.47.10", + "@types/csso": "^5.0.4", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/leo-profanity": "^1.5.4", @@ -39,6 +40,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "csso": "^5.0.5", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", @@ -51,7 +53,8 @@ "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "xss": "^1.0.15" } }, "node_modules/@ampproject/remapping": { @@ -2303,6 +2306,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/css-tree": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.9.tgz", + "integrity": "sha512-g1FE6xkPDP4tsccmTd6jIugjKZdxIDqAf9h2pc+4LsGgYbOyfa9phNjBHYbm6FtwIlNfT1NBx3f2zSeqO7aRAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/csso": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/csso/-/csso-5.0.4.tgz", + "integrity": "sha512-W/FsRkm/9c04x9ON+bj+HQ0cSgNkG1LvcfuBCpkP7cpikM7+RkrNFLGtiofb++xBG6KGMUycLoDbi9/K621ZCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/css-tree": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3314,27 +3334,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -4095,6 +4094,42 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -6975,6 +7010,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -8501,6 +8543,16 @@ "node": ">= 8" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -9713,6 +9765,30 @@ } } }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9792,4 +9868,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 381d894..cc90602 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@nestjs/testing": "^10.0.0", "@nestjs/throttler": "^6.3.0", "@supabase/supabase-js": "^2.47.10", + "@types/csso": "^5.0.4", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/leo-profanity": "^1.5.4", @@ -50,6 +51,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "csso": "^5.0.5", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", @@ -62,6 +64,7 @@ "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "xss": "^1.0.15" } } diff --git a/src/rices/rices.controller.ts b/src/rices/rices.controller.ts index 993b245..6a5ef82 100644 --- a/src/rices/rices.controller.ts +++ b/src/rices/rices.controller.ts @@ -10,6 +10,7 @@ import { HttpCode, HttpStatus, UnauthorizedException, + BadRequestException, } from '@nestjs/common'; import { RicesService } from './rices.service'; @@ -45,6 +46,9 @@ export class RicesController { ) { const contentString = typeof content === 'string' ? content : JSON.stringify(content); + + this.validateFileSize(contentString); // Validate file size + return this.ricesService.create(contentString, token, headers); } @@ -88,6 +92,9 @@ export class RicesController { ) { const contentString = typeof content === 'string' ? content : JSON.stringify(content); + + this.validateFileSize(contentString); // Validate file size + return this.ricesService.update(slug, token, contentString, headers); } @@ -121,4 +128,14 @@ export class RicesController { await this.ricesService.moderateRemove(slug); return; } + + private validateFileSize(content: string) { + const sizeInBytes = Buffer.byteLength(content, 'utf-8'); + const maxSizeInBytes = 1 * 1024 * 512; // 1 MB + if (sizeInBytes > maxSizeInBytes) { + throw new BadRequestException( + `The uploaded content exceeds the size limit of 512 KB.`, + ); + } + } } diff --git a/src/rices/rices.service.ts b/src/rices/rices.service.ts index e82bc9d..e0dbe62 100644 --- a/src/rices/rices.service.ts +++ b/src/rices/rices.service.ts @@ -5,6 +5,9 @@ import { ConflictException, BadRequestException, } from '@nestjs/common'; + +import xss from 'xss'; +import { minify } from 'csso'; import { v4 as uuidv4 } from 'uuid'; import { generateSlug } from './utils/slug.util'; import { ConfigService } from '@nestjs/config'; @@ -43,6 +46,10 @@ export class RicesService { try { this.validateJsonStructure(content); + + content = this.sanitizeJson(content); + content = this.minimizeJson(content); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new BadRequestException('Invalid json request'); @@ -190,6 +197,10 @@ export class RicesService { try { this.validateJsonStructure(content); + + content = this.sanitizeJson(content); + content = this.minimizeJson(content); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new BadRequestException('Invalid json request'); @@ -346,4 +357,38 @@ export class RicesService { return true; } + + // Método para minificar los campos CSS + private minimizeJson(jsonString: string): string { + const json = JSON.parse(jsonString); + + ['userChrome', 'userContent'].forEach((key) => { + if (json[key] && typeof json[key] === 'string') { + json[key] = minify(json[key]).css; + } + }); + + return JSON.stringify(json); + } + + private sanitizeJson(jsonString: string): string { + const json = JSON.parse(jsonString); + + const sanitizedJson = Object.keys(json).reduce( + (acc, key) => { + const value = json[key]; + if (typeof value === 'string') { + acc[key] = xss(value); // Limpia las cadenas de texto + } else if (typeof value === 'object' && value !== null) { + acc[key] = JSON.parse(this.sanitizeJson(JSON.stringify(value))); // Recursión para objetos anidados + } else { + acc[key] = value; // Otros tipos permanecen igual + } + return acc; + }, + {} as Record, + ); + + return JSON.stringify(sanitizedJson); + } }