{"id":"CVE-2026-115477","kind":"vulnerability","aliases":["CVE-2026-115477","GHSA-737x-cwfr-5vvc"],"related":[],"title":"obsidian-tc: Declined human-in-the-loop confirmations can permit destructive calls over HTTP","description":"### Summary\nOn a Streamable HTTP deployment with `auth.jwtSecret` configured, serving a 2026-07-28-era client that declared `elicitation.form` support, a call to a dispatch-gated tool (`destructive: true`, or a scope with an HITL floor) that was offered an `inputRequired` confirmation could still complete after the human declined, cancelled, or the elicitation response was omitted or malformed, provided the client echoed the validly bound `requestState` on resubmission. The server trusted an authentic, call-bound state as proof of approval; the state is minted before the elicitation leg is asked, so it proves only that the resubmission belongs to the same call.\n\n### Details\nReproduced directly: a `destructive: true` tool call, offered `inputRequired`, answered with `{action: \"decline\"}` on the `elicitation/create` leg, still wrote. Present since the 2026-07-28 round trip shipped in v1.13.0 (THE-583). Only dispatch-gated tools were reachable; the sixteen handler-side conditionally gated tools (`write_note` overwrite, `move_note`, and others) did not read the elicitation state on the modern wire at all, so they neither let a decline nor an approval through.\n\n### Impact\nA client that echoes state after a decline can execute destructive vault operations (delete, cross-folder move, frontmatter replace, link rewrite) that the human explicitly refused. The precondition is a compliant client behaving incorrectly or a client under an attacker's control; the human's refusal is not enforced by the server.\n\n### Patches\nFixed in 1.31.4: the server now also requires the confirm leg's own response (`inputResponse`) to be `{action: \"accept\", content: {approve: true}}` before trusting the echoed state. A declined, cancelled or not-approved round renders the ordinary `elicit_required` error and is never offered a second round for the same declined confirmation. The same release adds server-initiated in-band confirmation on stdio (legacy era), so stdio agents no longer need to mint `elicit_token` themselves.\n\n### Not addressed\nA malicious or non-compliant client can still answer its own `elicitation/create` leg with an approval regardless of what the human said. The server cannot verify that the client rendered the form or that a human saw it; this is inherent to the client-driven half of the 2026-07-28 protocol. The legacy shim, where the server sends and owns the leg, does not share this gap.\n\n### Workarounds\nRun affected deployments over stdio, or leave `auth.jwtSecret` unset so the `requestState` codec (and therefore `inputRequired`) is never offered; both fall back to the CLI `elicit_token` path.\n\n### Credit\nReported in GH #967; found and closed under THE-1106 (PR #969); the residual is documented in SECURITY.md.\n\n### References\n- PR #969, GH #967, CHANGELOG `## [1.31.4] ### Security`\n- `packages/server/src/mcp/server.ts` (`tools/call` handler), `packages/server/src/vault/hitl.ts`","summary":"obsidian-tc (npm, versions 1.13.0 through 1.31.3) lets a client that declined a human-in-the-loop confirmation still run destructive vault tools such as delete and move, on Streamable HTTP deployments with auth.jwtSecret set. An attacker controlling the client, or a non-compliant client, can replay the echoed requestState to execute operations the human refused.","severity":"high","cvss":7.1,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"obsidian-tc","fixed":["1.31.4"],"affected":">= 1.13.0, <= 1.31.3","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["cwe-285","cwe-863","mcp","obsidian-tc","package","self-hosted","streamable-http"],"not_affected_if":["jwt-secret-unset","managed-hosting","stdio-transport","version-1.31.4-or-later"],"how_to_check":"Check the installed obsidian-tc version with npm ls obsidian-tc; affected if between 1.13.0 and 1.31.3 inclusive.\nCheck whether the server runs Streamable HTTP with auth.jwtSecret configured in its config.","fix":"Upgrade obsidian-tc to 1.31.4.\nAs an alternative, run over stdio or leave auth.jwtSecret unset, per the advisory workarounds.","status":"open","published_at":"2026-10-23T15:17:29+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","triaged_at":"2026-10-23T20:03:38+00:00","updated_at":"2026-10-23T21:57:34+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T20:03:38Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade obsidian-tc to 1.31.4.\nAs an alternative, run over stdio or leave auth.jwtSecret unset, per the advisory workarounds.","summary":"obsidian-tc (npm, versions 1.13.0 through 1.31.3) lets a client that declined a human-in-the-loop confirmation still run destructive vault tools such as delete and move, on Streamable HTTP deployments with auth.jwtSecret set. An attacker controlling the client, or a non-compliant client, can replay the echoed requestState to execute operations the human refused.","category":"package","who_cares":["obsidian-tc","mcp","streamable-http","self-hosted"],"confidence":0.8,"analyst_note":"Residual gap: a malicious client can still self-approve the elicitation leg per the advisory's Not addressed section; confirm the 1.31.4 fix in PR #969 at https://github.com/advisories/GHSA-737x-cwfr-5vvc and decide whether to add a mitigation note for that residual.","how_to_check":"Check the installed obsidian-tc version with npm ls obsidian-tc; affected if between 1.13.0 and 1.31.3 inclusive.\nCheck whether the server runs Streamable HTTP with auth.jwtSecret configured in its config.\nunknown","needs_analyst":true,"not_affected_if":["stdio-transport","jwt-secret-unset","version-1.31.4-or-later","managed-hosting"],"severity_if_unknown":"high"},"cost_usd":0.00056,"input_tokens":2988,"output_tokens":522,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-07':117C,233C,473C '-1106':531C '-115477':19A,27A '-2026':18A,26A '-28':118C,234C,474C '-285':32B '-583':241C '-863':34B '1.13.0':51B '1.31.3':53B '1.31.4':343C,546C '2026':116C,232C,472C '5vvc':24A '737x':22A '967':525C,544C '969':533C,542C 'accept':359C 'action':221C,358C 'add':397C 'address':421C 'affect':494C 'agent':411C 'also':347C 'answer':219C,431C 'approv':186C,284C,361C,374C,438C 'ask':196C 'attack':84B,327C 'auth.jwtsecret':81B,112C,500C 'authent':178C 'back':514C 'band':403C 'behav':320C 'belong':204C 'bound':170C,181C 'call':14A,126C,180C,208C,216C 'call-bound':179C 'cancel':155C,370C 'cannot':447C 'changelog':545C 'cli':517C 'client':56B,87B,93B,120C,166C,288C,319C,324C,428C,451C,467C 'client-driven':466C 'close':528C 'codec':505C 'complet':150C 'compliant':92B,318C,427C 'condit':254C 'configur':113C 'confirm':10A,65B,147C,350C,393C,404C 'content':360C 'control':85B,329C 'could':148C 'credit':521C 'cross':302C 'cross-fold':301C 'cve':17A,25A 'cwe':31B,33B 'cwfr':23A 'declar':122C 'declin':4A,58B,154C,222C,281C,294C,369C,392C 'delet':73B,300C 'deploy':79B,110C,495C 'destruct':13A,68B,133C,213C,297C 'detail':209C 'direct':211C 'dispatch':130C,244C 'dispatch-g':129C,243C 'document':537C 'driven':468C 'echo':97B,167C,290C,366C 'elicit':158C,193C,268C,379C,417C,518C 'elicitation.form':123C 'elicitation/create':225C,434C 'enforc':336C 'era':119C,408C 'error':381C 'execut':100B,296C 'explicit':312C 'fall':513C 'fix':341C 'floor':141C 'folder':303C 'form':454C 'found':526C 'frontmatt':305C 'gap':491C 'gate':131C,245C,255C 'gh':524C,543C 'ghsa':21A 'ghsa-737x-cwfr-5vvc':20A 'half':469C 'handler':252C,550C 'handler-sid':251C 'hitl':140C 'host':42B 'http':16A,45B,78B,109C 'human':6A,61B,103B,153C,311C,331C,443C,458C 'human-in-the-loop':5A,60B 'impact':286C 'in-band':401C 'incorrect':321C 'inher':463C 'initi':400C 'inputrequir':146C,218C,508C 'inputrespons':355C 'leav':499C 'leg':194C,226C,351C,435C,486C 'legaci':407C,477C 'let':54B,279C 'link':307C 'longer':413C 'loop':9A,64B 'malform':163C 'malici':423C 'mcp':35B 'mint':190C,416C 'modern':272C 'move':75B,260C,304C 'need':414C 'neither':278C 'never':384C,510C 'non':91B,426C 'non-compli':90B,425C 'not-approv':372C 'note':258C,261C 'npm':49B 'obsidian':2A,29A,37B,47B 'obsidian-tc':1A,28A,36B,46B 'offer':144C,217C,385C,511C 'omit':161C 'oper':101B,299C 'ordinari':378C 'other':263C 'overwrit':259C 'own':484C 'packag':39B 'packages/server/src/mcp/server.ts':548C 'packages/server/src/vault/hitl.ts':551C 'patch':340C 'path':520C 'permit':12A 'pr':532C,541C 'precondit':315C 'present':229C 'proof':184C 'protocol':475C 'prove':199C 'provid':164C 'reachabl':248C 'read':266C 'refer':540C 'refus':104B,313C,333C 'regardless':439C 'releas':396C 'render':376C,452C 'replac':306C 'replay':95B 'report':522C 'reproduc':210C 'requestst':98B,171C,504C 'requir':348C,380C 'residu':535C 'respons':159C,354C 'resubmiss':173C,203C 'rewrit':308C 'round':235C,375C,388C 'run':67B,493C 'said':444C 'saw':459C 'scope':137C 'second':387C 'secur':547C 'security.md':539C 'self':41B 'self-host':40B 'send':482C 'serv':114C 'server':175C,339C,345C,399C,446C,481C 'server-initi':398C 'set':82B 'share':489C 'shim':478C 'ship':237C 'side':253C 'sinc':230C 'sixteen':250C 'state':182C,188C,269C,291C,367C 'stdio':406C,410C,497C 'still':66B,149C,227C,430C 'streamabl':44B,77B,108C 'streamable-http':43B 'summari':105C 'support':124C 'tc':3A,30A,38B,48B 'therefor':507C 'token':418C,519C 'tool':70B,132C,215C,246C,256C 'tools/call':549C 'trip':236C 'true':134C,214C,362C 'trust':176C,364C 'unset':501C 'v1.13.0':239C 'valid':169C 'vault':69B,298C 'verifi':448C 'version':50B 'wire':273C 'workaround':492C 'write':257C 'wrote':228C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-23T15:17:29+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-737x-cwfr-5vvc","url":"https://github.com/advisories/GHSA-737x-cwfr-5vvc","severity":"high","published_at":"2026-10-23T17:30:10+00:00","modified_at":"2026-10-23T17:30:11+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","fetched_at":"2026-10-23T19:11:31+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-115477","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-115477","severity":null,"published_at":"2026-10-23T15:17:29+00:00","modified_at":"2026-10-23T15:17:29+00:00","first_seen_at":"2026-10-23T21:57:34+00:00","fetched_at":"2026-10-23T22:29:03+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}