From 0f232bddccddae06d551873ed9c20f54a3de9882 Mon Sep 17 00:00:00 2001 From: Boot Date: Tue, 10 Mar 2026 03:44:39 +0800 Subject: [PATCH] fix: add app_id scope check to V1 API handlers (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V1 item handlers (handleGetItem, handleAddMessage, handleAssignItem, handleResolveItem, handleVerifyItem, handleReopenItem, handleCloseItem, handleRespondToItem) did not verify that the requested item belongs to the authenticated user's app_id. This allowed any authenticated user to read, modify, or close items belonging to other apps. Add the same app_id scoping check used by V2 endpoints: if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) → 403 Forbidden For handlers that previously skipped the item fetch (assign, resolve, verify, reopen, close), an explicit getItem() call is added before the scope check. Co-Authored-By: Claude Opus 4.6 --- server/index.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/server/index.js b/server/index.js index e5b751d..0530adf 100644 --- a/server/index.js +++ b/server/index.js @@ -592,6 +592,9 @@ function handleGetItems(req, res) { function handleGetItem(req, res) { const item = itemsDb.getItem(req.params.id); if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } res.json(item); } @@ -635,6 +638,9 @@ function handleAddMessage(req, res) { const item = itemsDb.getItem(req.params.id); if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } const result = itemsDb.addMessage({ item_id: req.params.id, @@ -650,6 +656,11 @@ function handleAddMessage(req, res) { function handleAssignItem(req, res) { const { assignee } = req.body; if (!assignee) return res.status(400).json({ error: 'Missing assignee' }); + const item = itemsDb.getItem(req.params.id); + if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } const result = itemsDb.assignItem(req.params.id, assignee); if (!result.changes) return res.status(404).json({ error: 'Item not found' }); sendWebhook('item.assigned', { id: req.params.id, assignee }); @@ -658,6 +669,11 @@ function handleAssignItem(req, res) { // -- POST /items/:id/resolve function handleResolveItem(req, res) { + const item = itemsDb.getItem(req.params.id); + if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } const result = itemsDb.resolveItem(req.params.id); if (!result.changes) return res.status(404).json({ error: 'Item not found' }); sendWebhook('item.resolved', { id: req.params.id }); @@ -666,6 +682,11 @@ function handleResolveItem(req, res) { // -- POST /items/:id/verify function handleVerifyItem(req, res) { + const item = itemsDb.getItem(req.params.id); + if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } const result = itemsDb.verifyItem(req.params.id); if (!result.changes) return res.status(404).json({ error: 'Item not found' }); res.json({ success: true }); @@ -673,6 +694,11 @@ function handleVerifyItem(req, res) { // -- POST /items/:id/reopen function handleReopenItem(req, res) { + const item = itemsDb.getItem(req.params.id); + if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } const result = itemsDb.reopenItem(req.params.id); if (!result.changes) return res.status(404).json({ error: 'Item not found' }); res.json({ success: true }); @@ -680,6 +706,11 @@ function handleReopenItem(req, res) { // -- POST /items/:id/close function handleCloseItem(req, res) { + const item = itemsDb.getItem(req.params.id); + if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } const result = itemsDb.closeItem(req.params.id); if (!result.changes) return res.status(404).json({ error: 'Item not found' }); res.json({ success: true }); @@ -692,6 +723,9 @@ function handleRespondToItem(req, res) { const item = itemsDb.getItem(req.params.id); if (!item) return res.status(404).json({ error: 'Item not found' }); + if (req.v2Auth?.app_id && item.app_id !== req.v2Auth.app_id) { + return res.status(403).json({ error: 'Access denied — item belongs to a different app' }); + } itemsDb.respondToItem(req.params.id, response); res.json({ success: true });