T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- index.js:541
- Finding
- Telegram Callback Handler Does Not Enforce the Configured User Allowlist<![CDATA[ ## Vulnerability Details **File Location**: `index.js:541-586` **Vulnerability Type**: Missing authorization for state-changing Telegram callbacks **Risk Level**: Medium ### Vulnerable Code ```javascript async function togglePending(state, itemId, target, account, threadId) { const item = state.items[itemId]; if (!item) throw new Error('Grocery item not found.'); item.status = item.status === STATUS_NEEDED ? STATUS_HAVE : STATUS_NEEDED; item.updated_at = utcNow(); await updateAllViews(state, account); return { ok: true, item: { id: item.id, name: item.name, status: item.status } }; } async function handleCallback(state, callback, target, account, threadId) { const parsed = parseCallback(callback); if (!parsed) throw new Error('Unsupported callback payload.'); const [action, value] = parsed; const view = resolveView(state, account, target, threadId); if (action === CALLBACK_TOGGLE) { return togglePending(state, value, target, account, threadId); } if (action === CALLBACK_VIEW) { const mode = value === VIEW_ALL ? VIEW_ALL : VIEW_NEEDED; if (mode === VIEW_NEEDED) view.session_ids = sortedItems(state, STATUS_NEEDED).map(i => i.id); if (view.message_id) return editExistingView(state, target, account, threadId, mode); return sendTelegramView(state, target, account, mode, threadId); } throw new Error('Unsupported callback action.'); } export default function register(api) { api.registerInteractiveHandler({ channel: 'telegram', namespace: 'gchk', handler: async ({ callback, senderId }) => { const target = String(callback.chatId || senderId); const fp = statePath(); let state; try { state = loadState(fp); } catch (err) { console.error('[grocery-checklist] state load error:', String(err)); return; } try { await ha ...[truncated 2815 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Load the `grocery` Telegram account configuration before processing callbacks. 2. Convert every `allowFrom` entry to a canonical string and reject the request unless `String(senderId)` is explicitly present. 3. Fail closed if the account configuration or allowlist cannot be loaded. 4. Verify that the callback's chat identifier corresponds to a stored view belonging to the same account and authorized sender. 5. Avoid treating deterministic item IDs as authorization credentials. 6. Consider binding callback data to a specific account, chat, message, and user through an opaque random value or an authenticated message code. 7. Log rejected callbacks without logging the bot token or other credentials. 8. Add tests proving that unauthorized senders cannot toggle items or switch views. Example authorization control: ```javascript function allowedTelegramUsers(account) { const config = JSON.parse(readFileSync(openclawConfigPath(), 'utf-8')); const allowFrom = config?.channels?.telegram?.accounts?.[account]?.allowFrom || []; return new Set(allowFrom.map(String)); } handler: async ({ callback, senderId }) => { const account = 'grocery'; const sender = String(senderId); if (!allowedTelegramUsers(account).has(sender)) { console.warn('[grocery-checklist] rejected unauthorized callback'); return; } // Continue processing only after authorization succeeds. } ``` ]]>
