diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index e7af06e..7edbf72 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -14,7 +14,7 @@ const authenticateToken = async (req, res, next) => { } try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); + const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET); const userId = decoded.id; if (!userId) { @@ -78,7 +78,7 @@ const optionalAuth = async (req, res, next) => { } try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); + const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET); const userId = decoded.id; if (!userId) { diff --git a/backend/middleware/csrf.js b/backend/middleware/csrf.js index ec1b093..0d788bd 100644 --- a/backend/middleware/csrf.js +++ b/backend/middleware/csrf.js @@ -72,7 +72,8 @@ const getCSRFToken = (req, res) => { maxAge: 60 * 60 * 1000, }); - res.json({ csrfToken: token }); + res.set("X-CSRF-Token", token); + res.status(204).send(); }; module.exports = { diff --git a/backend/package-lock.json b/backend/package-lock.json index a40c08e..39e1ca7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4067,22 +4067,43 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bowser": { @@ -4181,6 +4202,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4745,9 +4767,10 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -5359,27 +5382,18 @@ } }, "node_modules/express-validator": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", - "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", - "validator": "~13.12.0" + "validator": "~13.15.23" }, "engines": { "node": ">= 8.0.0" } }, - "node_modules/express-validator/node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5789,9 +5803,10 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -6006,26 +6021,23 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-proxy-agent": { @@ -7145,9 +7157,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -8454,17 +8466,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/react-is": { @@ -9812,6 +9841,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -9917,9 +9947,10 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", "engines": { "node": ">= 0.10" } diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a66edf6..05ab5d3 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -128,13 +128,13 @@ router.post( const token = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion }, - process.env.JWT_SECRET, + process.env.JWT_ACCESS_SECRET, { expiresIn: "15m" } // Short-lived access token ); const refreshToken = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, - process.env.JWT_SECRET, + process.env.JWT_REFRESH_SECRET, { expiresIn: "7d" } ); @@ -223,13 +223,13 @@ router.post( const token = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion }, - process.env.JWT_SECRET, + process.env.JWT_ACCESS_SECRET, { expiresIn: "15m" } // Short-lived access token ); const refreshToken = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, - process.env.JWT_SECRET, + process.env.JWT_REFRESH_SECRET, { expiresIn: "7d" } ); @@ -392,13 +392,13 @@ router.post( // Generate JWT tokens const token = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion }, - process.env.JWT_SECRET, + process.env.JWT_ACCESS_SECRET, { expiresIn: "15m" } ); const refreshToken = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, - process.env.JWT_SECRET, + process.env.JWT_REFRESH_SECRET, { expiresIn: "7d" } ); @@ -550,7 +550,7 @@ router.post( }); } - const decoded = jwt.verify(accessToken, process.env.JWT_SECRET); + const decoded = jwt.verify(accessToken, process.env.JWT_ACCESS_SECRET); const user = await User.findByPk(decoded.id); if (!user) { @@ -625,7 +625,7 @@ router.post("/refresh", async (req, res) => { } // Verify refresh token - const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); + const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET); if (!decoded.id || decoded.type !== "refresh") { return res.status(401).json({ error: "Invalid refresh token" }); @@ -648,7 +648,7 @@ router.post("/refresh", async (req, res) => { // Generate new access token const newAccessToken = jwt.sign( { id: user.id, jwtVersion: user.jwtVersion }, - process.env.JWT_SECRET, + process.env.JWT_ACCESS_SECRET, { expiresIn: "15m" } ); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index f7f2d33..21fc7d4 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -392,7 +392,8 @@ router.get('/images/:filename', helmet.crossOriginResourcePolicy({ policy: "cross-origin" }), async (req, res) => { try { - const { filename } = req.params; + // Sanitize filename to prevent path traversal attacks + const filename = path.basename(req.params.filename); // Verify user is sender or receiver of a message with this image const message = await Message.findOne({ diff --git a/backend/server.js b/backend/server.js index eb200c3..7a7a813 100644 --- a/backend/server.js +++ b/backend/server.js @@ -108,6 +108,7 @@ app.use( origin: process.env.FRONTEND_URL || "http://localhost:3000", credentials: true, optionsSuccessStatus: 200, + exposedHeaders: ["X-CSRF-Token"], }) ); diff --git a/backend/sockets/socketAuth.js b/backend/sockets/socketAuth.js index b2fce50..fcdee88 100644 --- a/backend/sockets/socketAuth.js +++ b/backend/sockets/socketAuth.js @@ -38,8 +38,8 @@ const authenticateSocket = async (socket, next) => { return next(new Error("Authentication required")); } - // Verify JWT - const decoded = jwt.verify(token, process.env.JWT_SECRET); + // Verify JWT (access tokens only) + const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET); const userId = decoded.id; if (!userId) { diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 89616e8..317b175 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -39,7 +39,7 @@ const api = axios.create({ export const fetchCSRFToken = async (): Promise => { try { const response = await api.get("/auth/csrf-token"); - csrfToken = response.data.csrfToken || ""; + csrfToken = response.headers["x-csrf-token"] || ""; return csrfToken || ""; } catch (error) { console.error("Failed to fetch CSRF token:", error); @@ -286,10 +286,14 @@ export const forumAPI = { deleteComment: (commentId: string) => api.delete(`/forum/comments/${commentId}`), // Admin endpoints - adminDeletePost: (id: string, reason: string) => api.delete(`/forum/admin/posts/${id}`, { data: { reason } }), - adminRestorePost: (id: string) => api.patch(`/forum/admin/posts/${id}/restore`), - adminDeleteComment: (id: string, reason: string) => api.delete(`/forum/admin/comments/${id}`, { data: { reason } }), - adminRestoreComment: (id: string) => api.patch(`/forum/admin/comments/${id}/restore`), + adminDeletePost: (id: string, reason: string) => + api.delete(`/forum/admin/posts/${id}`, { data: { reason } }), + adminRestorePost: (id: string) => + api.patch(`/forum/admin/posts/${id}/restore`), + adminDeleteComment: (id: string, reason: string) => + api.delete(`/forum/admin/comments/${id}`, { data: { reason } }), + adminRestoreComment: (id: string) => + api.patch(`/forum/admin/comments/${id}/restore`), adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`), adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`), };