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).
This commit is contained in:
oscargonzalezmoreno@gmail.com 2024-12-28 17:42:17 +01:00
parent 9bf1fe8e0d
commit aaee70f6df
4 changed files with 165 additions and 24 deletions

120
package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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.`,
);
}
}
}

View file

@ -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<string, unknown>,
);
return JSON.stringify(sanitizedJson);
}
}