From 76088cc76aedae709f06deaee2244efcf6a22bed Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Fri, 13 Jan 2023 10:16:35 +0100 Subject: [PATCH] feat: add ClamAV to scan for malicious files --- Dockerfile | 2 +- README.md | 15 +- backend/package-lock.json | 252 ++++++++---------- backend/package.json | 4 +- .../migration.sql | 2 + backend/prisma/schema.prisma | 11 +- backend/src/app.module.ts | 2 + backend/src/clamscan/clamscan.module.ts | 10 + backend/src/clamscan/clamscan.service.ts | 86 ++++++ backend/src/main.ts | 2 +- backend/src/share/share.module.ts | 8 +- backend/src/share/share.service.ts | 14 +- docker-compose-dev.yml | 7 + docker-compose.yml | 7 +- .../components/account/showShareLinkModal.tsx | 6 +- frontend/src/components/upload/Dropzone.tsx | 5 +- frontend/src/pages/share/[shareId].tsx | 20 +- frontend/src/types/File.type.ts | 2 +- 18 files changed, 284 insertions(+), 171 deletions(-) create mode 100644 backend/prisma/migrations/20230113080918_removed_reason_attribute/migration.sql create mode 100644 backend/src/clamscan/clamscan.module.ts create mode 100644 backend/src/clamscan/clamscan.service.ts create mode 100644 docker-compose-dev.yml diff --git a/Dockerfile b/Dockerfile index 5c203bf73..bcdb42a35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN npm run build && npm prune --production # Stage 5: Final image FROM node:18-slim AS runner -ENV NODE_ENV=production +ENV NODE_ENV=docker RUN apt-get update && apt-get install -y openssl WORKDIR /opt/app/frontend diff --git a/README.md b/README.md index 6c518352c..558fd694d 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,12 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran ## ✨ Features -- Spin up your instance within 2 minutes - Create a share with files that you can access with a link - No file size limit, only your disk will be your limit - Set a share expiration - Optionally secure your share with a visitor limit and a password - Email recepients -- Light & dark mode +- ClamAV integration ## 🐧 Get to know Pingvin Share @@ -30,6 +29,18 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧! +### Integrations + +#### ClamAV + +With ClamAV the shares get scanned for malicious files and get removed if any found. + +1. Add the ClamAV container to the Docker Compose stack (see `docker-compose.yml`) and start the container. +2. As soon as the ClamAV container is ready (when ClamAV logs "socket found, clamd started"), restart the Pingvin Share container with `docker compose restart pingvin-share` +3. The Pingvin Share logs should now log "ClamAV is active" + +Please note that ClamAV needs a lot of [ressources](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements). + ### Additional resources - [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/) diff --git a/backend/package-lock.json b/backend/package-lock.json index dc5788627..c4d75b1b0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "@nestjs/common": "^9.2.1", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.2.1", - "@nestjs/jwt": "^9.0.0", + "@nestjs/jwt": "^10.0.1", "@nestjs/mapped-types": "^1.2.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.2.1", @@ -21,6 +21,7 @@ "archiver": "^5.3.1", "argon2": "^0.30.2", "body-parser": "^1.20.1", + "clamscan": "^2.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "content-disposition": "^0.5.4", @@ -43,6 +44,7 @@ "@nestjs/schematics": "^9.0.3", "@nestjs/testing": "^9.2.1", "@types/archiver": "^5.3.1", + "@types/clamscan": "^2.0.4", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.0", "@types/express": "^4.17.14", @@ -674,12 +676,12 @@ } }, "node_modules/@nestjs/jwt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", - "integrity": "sha512-ZsXGY/wMYKzEhymw2+dxiwrHTRKIKrGszx6r2EjQqNLypdXMQu0QrujwZJ8k3+XQV4snmuJwwNakQoA2ILfq8w==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.1.tgz", + "integrity": "sha512-LwXBKVYHnFeX6GH/Wt0WDjsWCmNDC6tEdLlwNMAvJgYp+TkiCpEmQLkgRpifdUE29mvYSbjSnVs2kW2ob935NA==", "dependencies": { - "@types/jsonwebtoken": "8.5.8", - "jsonwebtoken": "8.5.1" + "@types/jsonwebtoken": "8.5.9", + "jsonwebtoken": "9.0.0" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0" @@ -1143,6 +1145,25 @@ "@types/node": "*" } }, + "node_modules/@types/clamscan": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.4.tgz", + "integrity": "sha512-NpD+EmE+ZK5WRJOAmeDuSYJIv15BUnc4PxQA+m3QNkutaPBZ7bmLDTvqBu2iDchs7YKQjiEQEwEMvsdwtdtImA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "axios": "^0.24.0" + } + }, + "node_modules/@types/clamscan/node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -1243,9 +1264,9 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", - "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", "dependencies": { "@types/node": "*" } @@ -2416,6 +2437,14 @@ "node": ">=6.0" } }, + "node_modules/clamscan": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clamscan/-/clamscan-2.1.2.tgz", + "integrity": "sha512-pcovgLHcrg3l/mI51Kuk0kN++07pSZdBTskISw0UFvsm8UXda8oNCm0eLeODxFg85Mz+k+TtSS9+XPlriJ8/Fg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", @@ -4467,9 +4496,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -4497,32 +4526,18 @@ } }, "node_modules/jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "dependencies": { "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.3.8" }, "engines": { - "node": ">=4", - "npm": ">=1.4.28" - } - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" + "node": ">=12", + "npm": ">=6" } }, "node_modules/jsprim": { @@ -4647,47 +4662,17 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -4737,9 +4722,9 @@ } }, "node_modules/luxon": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", - "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==", + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", + "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==", "engines": { "node": "*" } @@ -5464,11 +5449,11 @@ } }, "node_modules/passport-jwt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", - "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", "dependencies": { - "jsonwebtoken": "^8.2.0", + "jsonwebtoken": "^9.0.0", "passport-strategy": "^1.0.0" } }, @@ -6307,9 +6292,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -8056,12 +8041,12 @@ } }, "@nestjs/jwt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", - "integrity": "sha512-ZsXGY/wMYKzEhymw2+dxiwrHTRKIKrGszx6r2EjQqNLypdXMQu0QrujwZJ8k3+XQV4snmuJwwNakQoA2ILfq8w==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.1.tgz", + "integrity": "sha512-LwXBKVYHnFeX6GH/Wt0WDjsWCmNDC6tEdLlwNMAvJgYp+TkiCpEmQLkgRpifdUE29mvYSbjSnVs2kW2ob935NA==", "requires": { - "@types/jsonwebtoken": "8.5.8", - "jsonwebtoken": "8.5.1" + "@types/jsonwebtoken": "8.5.9", + "jsonwebtoken": "9.0.0" } }, "@nestjs/mapped-types": { @@ -8408,6 +8393,27 @@ "@types/node": "*" } }, + "@types/clamscan": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.4.tgz", + "integrity": "sha512-NpD+EmE+ZK5WRJOAmeDuSYJIv15BUnc4PxQA+m3QNkutaPBZ7bmLDTvqBu2iDchs7YKQjiEQEwEMvsdwtdtImA==", + "dev": true, + "requires": { + "@types/node": "*", + "axios": "^0.24.0" + }, + "dependencies": { + "axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.4" + } + } + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -8508,9 +8514,9 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", - "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", "requires": { "@types/node": "*" } @@ -9408,6 +9414,11 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "clamscan": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clamscan/-/clamscan-2.1.2.tgz", + "integrity": "sha512-pcovgLHcrg3l/mI51Kuk0kN++07pSZdBTskISw0UFvsm8UXda8oNCm0eLeODxFg85Mz+k+TtSS9+XPlriJ8/Fg==" + }, "class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", @@ -10973,9 +10984,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonc-parser": { @@ -10995,27 +11006,14 @@ } }, "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "requires": { "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } + "semver": "^7.3.8" } }, "jsprim": { @@ -11119,47 +11117,17 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -11196,9 +11164,9 @@ } }, "luxon": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", - "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==" + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", + "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==" }, "macos-release": { "version": "2.5.0", @@ -11729,11 +11697,11 @@ } }, "passport-jwt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", - "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", "requires": { - "jsonwebtoken": "^8.2.0", + "jsonwebtoken": "^9.0.0", "passport-strategy": "^1.0.0" } }, @@ -12344,9 +12312,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "requires": { "lru-cache": "^6.0.0" } diff --git a/backend/package.json b/backend/package.json index c818908bf..b72035b03 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,7 +16,7 @@ "@nestjs/common": "^9.2.1", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.2.1", - "@nestjs/jwt": "^9.0.0", + "@nestjs/jwt": "^10.0.1", "@nestjs/mapped-types": "^1.2.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.2.1", @@ -26,6 +26,7 @@ "archiver": "^5.3.1", "argon2": "^0.30.2", "body-parser": "^1.20.1", + "clamscan": "^2.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "content-disposition": "^0.5.4", @@ -48,6 +49,7 @@ "@nestjs/schematics": "^9.0.3", "@nestjs/testing": "^9.2.1", "@types/archiver": "^5.3.1", + "@types/clamscan": "^2.0.4", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.0", "@types/express": "^4.17.14", diff --git a/backend/prisma/migrations/20230113080918_removed_reason_attribute/migration.sql b/backend/prisma/migrations/20230113080918_removed_reason_attribute/migration.sql new file mode 100644 index 000000000..eff9ec809 --- /dev/null +++ b/backend/prisma/migrations/20230113080918_removed_reason_attribute/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Share" ADD COLUMN "removedReason" TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 35ca29d2b..d01b0dea8 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -52,11 +52,12 @@ model Share { id String @id @default(uuid()) createdAt DateTime @default(now()) - uploadLocked Boolean @default(false) - isZipReady Boolean @default(false) - views Int @default(0) - expiration DateTime - description String? + uploadLocked Boolean @default(false) + isZipReady Boolean @default(false) + views Int @default(0) + expiration DateTime + description String? + removedReason String? creatorId String? creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index cdff7c10e..5fbc3529e 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { JobsModule } from "./jobs/jobs.module"; import { PrismaModule } from "./prisma/prisma.module"; import { ShareModule } from "./share/share.module"; import { UserModule } from "./user/user.module"; +import { ClamscanModule } from "./clamscan/clamscan.module"; @Module({ imports: [ @@ -28,6 +29,7 @@ import { UserModule } from "./user/user.module"; limit: 100, }), ScheduleModule.forRoot(), + ClamscanModule, ], providers: [ { diff --git a/backend/src/clamscan/clamscan.module.ts b/backend/src/clamscan/clamscan.module.ts new file mode 100644 index 000000000..6a0d39351 --- /dev/null +++ b/backend/src/clamscan/clamscan.module.ts @@ -0,0 +1,10 @@ +import { forwardRef, Module } from "@nestjs/common"; +import { FileModule } from "src/file/file.module"; +import { ClamScanService } from "./clamscan.service"; + +@Module({ + imports: [forwardRef(() => FileModule)], + providers: [ClamScanService], + exports: [ClamScanService], +}) +export class ClamscanModule {} diff --git a/backend/src/clamscan/clamscan.service.ts b/backend/src/clamscan/clamscan.service.ts new file mode 100644 index 000000000..b1a53bf10 --- /dev/null +++ b/backend/src/clamscan/clamscan.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from "@nestjs/common"; +import * as NodeClam from "clamscan"; +import * as fs from "fs"; +import { FileService } from "src/file/file.service"; +import { PrismaService } from "src/prisma/prisma.service"; + +const clamscanConfig = { + clamdscan: { + host: process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1", + port: 3310, + localFallback: false, + }, + preference: "clamdscan", +}; + +@Injectable() +export class ClamScanService { + constructor( + private fileService: FileService, + private prisma: PrismaService + ) {} + + private ClamScan: Promise = new NodeClam() + .init(clamscanConfig) + .then((res) => { + console.log("ClamAV is active"); + return res; + }) + .catch(() => { + console.log("ClamAV is not active"); + return null; + }); + + async check(shareId: string) { + const clamScan = await this.ClamScan; + + if (!clamScan) return []; + + const infectedFiles = []; + + const files = fs + .readdirSync(`./data/uploads/shares/${shareId}`) + .filter((file) => file != "archive.zip"); + + for (const fileId of files) { + const { isInfected } = await clamScan + .isInfected(`./data/uploads/shares/${shareId}/${fileId}`) + .catch(() => { + console.log("ClamAV is not active"); + return { isInfected: false }; + }); + + const fileName = ( + await this.prisma.file.findUnique({ where: { id: fileId } }) + ).name; + + if (isInfected) { + infectedFiles.push({ id: fileId, name: fileName }); + } + } + + return infectedFiles; + } + + async checkAndRemove(shareId: string) { + const infectedFiles = await this.check(shareId); + + if (infectedFiles.length > 0) { + await this.fileService.deleteAllFiles(shareId); + await this.prisma.file.deleteMany({ where: { shareId } }); + + const fileNames = infectedFiles.map((file) => file.name).join(", "); + + await this.prisma.share.update({ + where: { id: shareId }, + data: { + removedReason: `Your share got removed because the file(s) ${fileNames} are malicious.`, + }, + }); + + console.log( + `Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)` + ); + } + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index 303cf1b6f..96d8714b2 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -11,7 +11,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe({ whitelist: true })); app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); - app.use(bodyParser.raw({type:'application/octet-stream', limit:'20mb'})); + app.use(bodyParser.raw({ type: "application/octet-stream", limit: "20mb" })); app.use(cookieParser()); app.set("trust proxy", true); diff --git a/backend/src/share/share.module.ts b/backend/src/share/share.module.ts index 8ca530458..718364e08 100644 --- a/backend/src/share/share.module.ts +++ b/backend/src/share/share.module.ts @@ -1,12 +1,18 @@ import { forwardRef, Module } from "@nestjs/common"; import { JwtModule } from "@nestjs/jwt"; +import { ClamscanModule } from "src/clamscan/clamscan.module"; import { EmailModule } from "src/email/email.module"; import { FileModule } from "src/file/file.module"; import { ShareController } from "./share.controller"; import { ShareService } from "./share.service"; @Module({ - imports: [JwtModule.register({}), EmailModule, forwardRef(() => FileModule)], + imports: [ + JwtModule.register({}), + EmailModule, + ClamscanModule, + forwardRef(() => FileModule), + ], controllers: [ShareController], providers: [ShareService], exports: [ShareService], diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index 8eb4edd3e..17e8623a9 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -10,6 +10,7 @@ import * as archiver from "archiver"; import * as argon from "argon2"; import * as fs from "fs"; import * as moment from "moment"; +import { ClamScanService } from "src/clamscan/clamscan.service"; import { ConfigService } from "src/config/config.service"; import { EmailService } from "src/email/email.service"; import { FileService } from "src/file/file.service"; @@ -23,7 +24,8 @@ export class ShareService { private fileService: FileService, private emailService: EmailService, private config: ConfigService, - private jwtService: JwtService + private jwtService: JwtService, + private clasmScanService: ClamScanService ) {} async create(share: CreateShareDTO, user?: User) { @@ -123,6 +125,9 @@ export class ShareService { ); } + // Check if any file is malicious with ClamAV + this.clasmScanService.checkAndRemove(share.id); + return await this.prisma.share.update({ where: { id }, data: { uploadLocked: true }, @@ -157,7 +162,7 @@ export class ShareService { } async get(id: string) { - const share: any = await this.prisma.share.findUnique({ + const share = await this.prisma.share.findUnique({ where: { id }, include: { files: true, @@ -165,10 +170,13 @@ export class ShareService { }, }); + if (share.removedReason) + throw new NotFoundException(share.removedReason, "share_removed"); + if (!share || !share.uploadLocked) throw new NotFoundException("Share not found"); - return share; + return share as any; } async getMetaData(id: string) { diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 000000000..505c9e715 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,7 @@ +version: '3.8' +services: + clamav: + restart: unless-stopped + ports: + - 3310:3310 + image: clamav/clamav \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 642965045..558d697a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,4 +6,9 @@ services: ports: - 3000:3000 volumes: - - "${PWD}/data:/opt/app/backend/data" + - "./data:/opt/app/backend/data" +# Optional: Add ClamAV (see README.md) +# ClamAV is currently only available for AMD64 see https://github.com/Cisco-Talos/clamav/issues/482 +# clamav: +# restart: unless-stopped +# image: clamav/clamav \ No newline at end of file diff --git a/frontend/src/components/account/showShareLinkModal.tsx b/frontend/src/components/account/showShareLinkModal.tsx index cfa3b1b8a..27a492ff1 100644 --- a/frontend/src/components/account/showShareLinkModal.tsx +++ b/frontend/src/components/account/showShareLinkModal.tsx @@ -1,7 +1,11 @@ import { Stack, TextInput } from "@mantine/core"; import { ModalsContextProps } from "@mantine/modals/lib/context"; -const showShareLinkModal = (modals: ModalsContextProps, shareId: string, appUrl : string) => { +const showShareLinkModal = ( + modals: ModalsContextProps, + shareId: string, + appUrl: string +) => { const link = `${appUrl}/share/${shareId}`; return modals.openModal({ title: "Share link", diff --git a/frontend/src/components/upload/Dropzone.tsx b/frontend/src/components/upload/Dropzone.tsx index 61e056d71..9f9f914cb 100644 --- a/frontend/src/components/upload/Dropzone.tsx +++ b/frontend/src/components/upload/Dropzone.tsx @@ -53,7 +53,10 @@ const Dropzone = ({ disabled={isUploading} openRef={openRef as ForwardedRef<() => void>} onDrop={(newFiles: FileUpload[]) => { - const fileSizeSum = [...newFiles, ...files].reduce((n, { size }) => n + size, 0); + const fileSizeSum = [...newFiles, ...files].reduce( + (n, { size }) => n + size, + 0 + ); if (fileSizeSum > config.get("MAX_SHARE_SIZE")) { toast.error( diff --git a/frontend/src/pages/share/[shareId].tsx b/frontend/src/pages/share/[shareId].tsx index ca2036794..a4526e881 100644 --- a/frontend/src/pages/share/[shareId].tsx +++ b/frontend/src/pages/share/[shareId].tsx @@ -47,21 +47,19 @@ const Share = ({ shareId }: { shareId: string }) => { .catch((e) => { const { error } = e.response.data; if (e.response.status == 404) { - showErrorModal( - modals, - "Not found", - "This share can't be found. Please check your link." - ); + if (error == "share_removed") { + showErrorModal(modals, "Share removed", e.response.data.message); + } else { + showErrorModal( + modals, + "Not found", + "This share can't be found. Please check your link." + ); + } } else if (error == "share_password_required") { showEnterPasswordModal(modals, getShareToken); } else if (error == "share_token_required") { getShareToken(); - } else if (error == "forbidden") { - showErrorModal( - modals, - "Forbidden", - "You're not allowed to see this share. Are you logged in with the correct account?" - ); } else { showErrorModal(modals, "Error", "An unknown error occurred."); } diff --git a/frontend/src/types/File.type.ts b/frontend/src/types/File.type.ts index e5defed4f..d5ccf6354 100644 --- a/frontend/src/types/File.type.ts +++ b/frontend/src/types/File.type.ts @@ -1,3 +1,3 @@ export type FileUpload = File & { uploadingProgress: number }; -export type FileUploadResponse = {id: string, name: string} \ No newline at end of file +export type FileUploadResponse = { id: string; name: string };