From bc8699afdcc55d146e87a44fbb8ff76bb60be93f Mon Sep 17 00:00:00 2001 From: Ann Rose Date: Tue, 19 May 2026 23:50:12 +0530 Subject: [PATCH] unathenticated end points (IDOR) solved --- backend/controllers/csvDownload.controller.js | 33 +++++++--- database.js | 2 + package-lock.json | 65 +++++++++++++++++++ package.json | 1 + server.js | 57 ++++++++++------ 5 files changed, 129 insertions(+), 29 deletions(-) diff --git a/backend/controllers/csvDownload.controller.js b/backend/controllers/csvDownload.controller.js index 5ad0b63..650567b 100644 --- a/backend/controllers/csvDownload.controller.js +++ b/backend/controllers/csvDownload.controller.js @@ -10,15 +10,30 @@ const { db } = require("../../database.js"); */ +// Add an escaping function to combat CSV injection and comma parsing issues +function escapeCSV(str) { + if (str === null || str === undefined) return '""'; + let stringified = String(str); + + // Prevent CSV Formula Injection by escaping =, +, -, @ + if (/^[=+\-@]/.test(stringified)) { + stringified = "'" + stringified; + } + + // Escape existing double quotes and wrap the whole string in quotes + return `"${stringified.replace(/"/g, '""')}"`; +} + async function downloadData(req, res) { try { const query = ` SELECT tasks.*, subjects.name AS subject_name FROM tasks LEFT JOIN subjects ON tasks.subject_id = subjects.id + WHERE tasks.user_id = ? `; const data = await new Promise((resolve, reject) => { - db.all(query, [], (err, rows) => { + db.all(query, [req.user.email], (err, rows) => { if (err) reject(err); else resolve(rows); }); @@ -27,14 +42,14 @@ async function downloadData(req, res) { const rows = [ ["Task ID", "Subject", "Title", "Due At", "Status", "Priority", "Confidence Score", "Notes"], ...data.map(task => [ - task.id, - task.subject_name, - task.title, - task.due_at, - task.status, - task.priority, - task.confidence_score, - `"${(task.notes || '').replace(/"/g, '""')}"` + escapeCSV(task.id), + escapeCSV(task.subject_name), + escapeCSV(task.title), + escapeCSV(task.due_at), + escapeCSV(task.status), + escapeCSV(task.priority), + escapeCSV(task.confidence_score), + escapeCSV(task.notes) ]) ]; diff --git a/database.js b/database.js index 79a5c67..42e494c 100644 --- a/database.js +++ b/database.js @@ -8,6 +8,7 @@ function initDb() { // Subjects Table db.run(`CREATE TABLE IF NOT EXISTS subjects ( id TEXT PRIMARY KEY, + user_id TEXT, name TEXT NOT NULL, short_code TEXT, color TEXT, @@ -17,6 +18,7 @@ function initDb() { // Tasks Table db.run(`CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, + user_id TEXT, subject_id TEXT, title TEXT NOT NULL, description TEXT, diff --git a/package-lock.json b/package-lock.json index d4124c7..1853f07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "sqlite3": "^6.0.1" }, "engines": { @@ -987,6 +988,28 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -1008,6 +1031,48 @@ "safe-buffer": "^5.0.1" } }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", diff --git a/package.json b/package.json index 4d37165..7dba89d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "sqlite3": "^6.0.1" }, "engines": { diff --git a/server.js b/server.js index 877f766..f9e6f61 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,7 @@ const cors = require('cors'); const { db, initDb } = require('./database'); const { GoogleGenAI } = require('@google/genai'); const path = require('path'); +const jwt = require('jsonwebtoken'); const csvDownloadRouter = require('./backend/routers/csvDownload.router.js'); const app = express(); @@ -13,6 +14,19 @@ app.use(express.json()); const page404Path = path.join(__dirname, '404.html'); const page500Path = path.join(__dirname, 'error.html'); +// Auth Middleware +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + if (token == null) return res.sendStatus(401); + + jwt.verify(token, process.env.JWT_SECRET || 'secret_key', (err, user) => { + if (err) return res.sendStatus(403); + req.user = user; + next(); + }); +} + // Static app.use('/css', express.static(path.join(__dirname, 'css'))); app.use('/js', express.static(path.join(__dirname, 'js'))); @@ -233,8 +247,8 @@ function nlpExtractTasksFromText(text) { // ============================================================ // ================= SUBJECTS ================= -app.get('/api/subjects', (req, res) => { - db.all('SELECT * FROM subjects', (err, rows) => { +app.get('/api/subjects', authenticateToken, (req, res) => { + db.all('SELECT * FROM subjects WHERE user_id = ?', [req.user.email], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); }); @@ -249,7 +263,7 @@ const ALLOWED_SUBJECT_COLORS = new Set([ 'var(--color-text-secondary)', ]); -app.post('/api/subjects', (req, res) => { +app.post('/api/subjects', authenticateToken, (req, res) => { const name = String(req.body?.name || '').trim(); let color = String(req.body?.color || '').trim() || 'var(--color-text-info)'; if (!name) { @@ -261,8 +275,8 @@ app.post('/api/subjects', (req, res) => { const shortCode = name.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 4) || 'SUB'; const id = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; db.run( - 'INSERT INTO subjects (id, name, short_code, color) VALUES (?, ?, ?, ?)', - [id, name, shortCode, color], + 'INSERT INTO subjects (id, user_id, name, short_code, color) VALUES (?, ?, ?, ?, ?)', + [id, req.user.email, name, shortCode, color], function (err) { if (err) return res.status(500).json({ error: err.message }); res.status(201).json({ id, name, short_code: shortCode, color }); @@ -271,15 +285,15 @@ app.post('/api/subjects', (req, res) => { }); // ================= TASKS ================= -app.get('/api/tasks', (req, res) => { - db.all('SELECT * FROM tasks ORDER BY due_at ASC', (err, rows) => { +app.get('/api/tasks', authenticateToken, (req, res) => { + db.all('SELECT * FROM tasks WHERE user_id = ? ORDER BY due_at ASC', [req.user.email], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows); }); }); // ================= ADD TASKS ================= -app.post('/api/tasks', (req, res) => { +app.post('/api/tasks', authenticateToken, (req, res) => { try { const tasks = Array.isArray(req.body) ? req.body : [req.body]; @@ -292,8 +306,8 @@ app.post('/api/tasks', (req, res) => { let errors = []; const stmt = db.prepare(`INSERT INTO tasks - (id, subject_id, title, due_at, status, priority, confidence_score, notes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`); + (id, user_id, subject_id, title, due_at, status, priority, confidence_score, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`); let pending = tasks.length; @@ -308,8 +322,8 @@ app.post('/api/tasks', (req, res) => { } db.get( - `SELECT * FROM tasks WHERE LOWER(title) = LOWER(?) AND subject_id = ? AND DATE(due_at) = DATE(?)`, - [t.title, t.subject_id, t.due_at], + `SELECT * FROM tasks WHERE LOWER(title) = LOWER(?) AND subject_id = ? AND DATE(due_at) = DATE(?) AND user_id = ?`, + [t.title, t.subject_id, t.due_at, req.user.email], (err, existing) => { if (err) { errors.push({ task: t, error: err.message }); @@ -323,6 +337,7 @@ app.post('/api/tasks', (req, res) => { const id = 'task_' + Date.now() + Math.random().toString(36).substr(2, 5); stmt.run( id, + req.user.email, t.subject_id, t.title, t.due_at, @@ -370,7 +385,7 @@ app.post('/api/tasks', (req, res) => { }); // ================= UPDATE ================= -app.put('/api/tasks/:id', (req, res) => { +app.put('/api/tasks/:id', authenticateToken, (req, res) => { const { status, archived, title, subject_id, due_at, notes, priority } = req.body; let query = 'UPDATE tasks SET '; @@ -389,8 +404,8 @@ app.put('/api/tasks/:id', (req, res) => { return res.status(400).json({ error: 'No fields to update' }); } - query += updates.join(', ') + ' WHERE id = ?'; - params.push(req.params.id); + query += updates.join(', ') + ' WHERE id = ? AND user_id = ?'; + params.push(req.params.id, req.user.email); db.run(query, params, function (err) { if (err) return res.status(500).json({ error: err.message }); @@ -399,10 +414,10 @@ app.put('/api/tasks/:id', (req, res) => { }); // ================= DELETE ================= -app.delete('/api/tasks/:id', (req, res) => { +app.delete('/api/tasks/:id', authenticateToken, (req, res) => { db.run( - 'DELETE FROM tasks WHERE id = ?', - [req.params.id], + 'DELETE FROM tasks WHERE id = ? AND user_id = ?', + [req.params.id, req.user.email], function (err) { if (err) return res.status(500).json({ error: err.message }); res.json({ success: true, changes: this.changes }); @@ -468,7 +483,9 @@ app.post('/api/auth/login', (req, res) => { if (!user || user.password !== password) { return res.status(401).json({ error: 'Invalid email or password' }); } - res.json({ success: true, email: user.email }); + + const token = jwt.sign({ email: user.email }, process.env.JWT_SECRET || 'secret_key', { expiresIn: '1h' }); + res.json({ success: true, token, email: user.email }); }); // Intentional test route for verifying server error page behavior. @@ -476,7 +493,7 @@ app.get('/debug/force-error', (req, res, next) => { next(new Error('Intentional test error')); }); -app.use('/api', csvDownloadRouter); +app.use('/api', authenticateToken, csvDownloadRouter); app.use('/api', (req, res) => { return res.status(404).json({ error: 'API route not found' });