Root cause
Table view ships in released packages
Published packages served the view from 0.4.40 through 0.4.48; 0.4.49 stopped registering it, which is narrower than the advisory's range.
Provisional finding. Full causal and release verification remains incomplete.
The CKAN MCP server's table view rendered posted messages without checking their origin and later posted replies to any origin. Output was HTML-escaped, so the effect is display spoofing, not script execution. Published packages served the view only in 0.4.40 to 0.4.48; 0.4.49 stopped registering it, narrower than the advisory's range.
AI-assisted change
The AI-attributed change introduces the table view and its message handling; it renders posted tool-result data without checking origin and later replies to any origin.
Root cause
Published packages served the view from 0.4.40 through 0.4.48; 0.4.49 stopped registering it, which is narrower than the advisory's range.
Fix
The view records the host origin from the initialize response, rejects data-bearing messages from other origins, and posts JSON-RPC replies to that pinned origin instead of '*'.
Case facts
Cite this finding
Vibe Security Radar, "CVE-2026-76894 in ondata/ckan-mcp-server," Georgia Tech SSLab, https://vibesecradar.com/cves/CVE-2026-76894/
+ bindEvents();+ }+ function bindEvents(){+ var filterEl=document.getElementById('filter');+ if(filterEl) filterEl.addEventListener('input',function(e){ filterText=e.target.value; currentPage=1; render(); });+ var pageSzEl=document.getElementById('pageSz');+ if(pageSzEl) pageSzEl.addEventListener('change',function(e){ pageSize=parseInt(e.target.value,10); currentPage=1; render(); });+ document.querySelectorAll('th[data-col]').forEach(function(th){+ th.addEventListener('click',function(){+ var col=th.getAttribute('data-col');+ if(sortCol===col){ sortDir=-sortDir; } else { sortCol=col; sortDir=1; }+ render();+ });+ });+ var p=document.getElementById('prevBtn');+ if(p) p.addEventListener('click',function(){ currentPage--; render(); });+ var n=document.getElementById('nextBtn');+ if(n) n.addEventListener('click',function(){ currentPage++; render(); });+ document.querySelectorAll('.page-btn[data-page]').forEach(function(btn){+ btn.addEventListener('click',function(){ currentPage=parseInt(btn.getAttribute('data-page'),10); render(); });+ });+ }+ function initTable(data){+ fields=data.fields||[];+ allRecords=data.records||[];+ total=data.total||allRecords.length;+ colTypes=detectColTypes(allRecords,fields);+ currentPage=1; sortCol=null; sortDir=1; filterText='';+ render();+ }+ window.addEventListener('message',function(event){+ var msg=event.data;+ if(!msg||typeof msg!=='object') return;+ var data=+ (msg._meta&&msg._meta.ui&&msg._meta.ui.data)||+ (msg.toolResult&&msg.toolResult._meta&&msg.toolResult._meta.ui&&msg.toolResult._meta.ui.data)||+ (msg.result&&msg.result._meta&&msg.result._meta.ui&&msg.result._meta.ui.data)||+ (msg.fields&&msg.records?msg:null);+ if(data&&(data.fields||data.records)) initTable(data);+ });+ }());+ </script>+</body>+</html>`;++export function registerDatastoreTableUiResource(server: McpServer) {+ server.registerResource(+ "ckan-datastore-table-ui",+ new ResourceTemplate("ckan-ui://datastore-table", { list: undefined }),+ {+ title: "CKAN DataStore Table Viewer",+ description: "Interactive sortable/filterable/paginated table for DataStore query results (MCP Apps UI)",+ mimeType: "text/html",+ },+ async (uri) => {+ return {+ contents: [+ {+ uri: uri.href,+ mimeType: "text/html",+ text: DATASTORE_TABLE_HTML,+ },+ ],+ };+ }+ );+}@@ -328,15 +328,19 @@ const DATASTORE_TABLE_HTML = `<!DOCTYPE html> } // SEP-1865 handshake: host won't send tool-result until initialized var _pending={};var _nid=1;+ // Pin the host origin from the initialize response, then only accept messages+ // from it and post back to it (never '*') — postMessage origin hardening.+ var _hostOrigin=null;+ function target(){return _hostOrigin||'*';} function mcpReq(method,params){ var id=_nid++; return new Promise(function(res,rej){ _pending[id]={resolve:res,reject:rej};- window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},'*');+ window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},target()); }); } function mcpNot(method,params){- window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*');+ window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},target()); } var _inited=false; function sendInited(){if(_inited)return;_inited=true;mcpNot('ui/notifications/initialized');}@@ -346,11 +350,15 @@ const DATASTORE_TABLE_HTML = `<!DOCTYPE html> window.addEventListener('message',function(event){ var msg=event.data; if(!msg||typeof msg!=='object') return;+ // Reply to a pending request: pin the responder's origin as the trusted host. if(msg.id!==undefined&&_pending[msg.id]){+ if(_hostOrigin===null)_hostOrigin=event.origin; var r=_pending[msg.id];delete _pending[msg.id]; if(msg.error)r.reject(new Error(JSON.stringify(msg.error)));else r.resolve(msg.result); return; }+ // Data-bearing messages must come from the pinned host origin.+ if(_hostOrigin===null||event.origin!==_hostOrigin) return; var data=null; if(msg.method==='ui/notifications/tool-result'&&msg.params&&msg.params.structuredContent){ data=msg.params.structuredContent;AI-assisted change 667a424f3104168b0bd13d4c54aebbbbc2bd5ef2f0c71c37f283e64fa158c87a · Fix cf563674deb213686ebd0e01fa85158eb39054f40afb28e0d547de8179e4f597