This is a side door left cracked open on a service that was supposed to check badges at the front
CVE-2026-9739 is a DNS rebinding / CORS policy bypass in Google MCP Toolbox for Databases when the server is used over HTTP+SSE under MCP spec v2024-11-05. The SSE initialization handler kept a hardcoded Access-Control-Allow-Origin: *, which neutered the product's own --allowed-origins protection and let a malicious website talk to a victim's Toolbox instance through the browser. The fix landed in v1.2.0; based on release history, the vulnerable window is at least pre-1.2.0 builds using SSE, with beta-era builds likely included.
The scary part is what the Toolbox can reach: databases, cloud data sources, and whatever tools the operator wired into it. But the vendor-style CRITICAL/9.4 framing overstates enterprise reality because exploitation is not a clean unauthenticated internet service hit. It requires a victim running Toolbox over SSE, a browser session, a malicious page, and a reachable local or internal HTTP endpoint. That's a meaningful chain of prerequisites, so this belongs in MEDIUM, not in your top emergency patch lane.
4 steps from start to impact.
Lure a user with Toolbox open
127.0.0.1:5000 or an internal host/port. This is user-interaction-dependent by design.- Victim browses to attacker-controlled content
- Toolbox is running at the time of the visit
- HTTP transport is in use rather than
--stdio
- No browser visit, no exploit
- A lot of enterprise MCP usage stays on local stdio rather than remote SSE
- Default listen address is
127.0.0.1, which narrows reach to browser-mediated attacks instead of raw internet exploitation
Bypass origin policy on the SSE endpoint
Access-Control-Allow-Origin: *. That defeats the admin's intended --allowed-origins restriction and gives the page a foothold into the MCP transport. In the issue report, the impact is described as enabling session hijack and arbitrary tool use on the victim's behalf.- Affected build before
v1.2.0 - SSE path in use under MCP spec
v2024-11-05 - Browser can reach the Toolbox host and port
- If the instance uses
--stdio, this path disappears - If the service is patched to
v1.2.0+, the hardcoded wildcard is removed - Host validation and strict origin controls reduce reach even before patching
Rebind and ride the victim's session
- Attacker controls DNS for the lure domain
- Victim browser accepts the rebinding sequence
- Toolbox session semantics are available through SSE
- Timing and browser behavior make this less reliable than straight socket exploitation
- Enterprise browser isolation or hardened DNS controls can break the chain
- Some deployments may require additional auth before useful tools can be called
Invoke data-access tools through Toolbox
execute_sql, BigQuery access, or other database-backed actions defined in tools.yaml. Impact is therefore highly environment-specific: one dev box may expose a demo database; another may front production analytics.- Useful tools are configured in Toolbox
- Connected data sources are reachable with the victim's trust context
- Authz inside the tools is weak enough to permit meaningful reads or actions
- Blast radius is limited to what that one Toolbox instance is configured to reach
- Least-privilege DB identities materially reduce impact
- Some deployments expose read-only tooling rather than write-capable operations
The supporting signals.
| In-the-wild status | No public evidence of active exploitation surfaced in the reviewed sources, and the supplied intel says KEV: No. |
|---|---|
| Proof-of-concept availability | No standalone weaponized PoC repo found. The exploit primitive is effectively documented in issue #3053, which spells out session hijack and arbitrary tool use. |
| EPSS | 0.00018 per the supplied intel and mirrored by Tenable; that is extremely low and consistent with a niche, precondition-heavy chain. |
| KEV status | Not KEV-listed in the supplied intel. No public campaign reporting was found in reviewed references. |
| CVSS state | Contrary to the prompt's baseline note, NVD now shows a Google CNA CVSS v4 of 9.4 CRITICAL with vector CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H (NVD). I think that score overweighted technical impact and underweighted deployment friction. |
| Affected versions | Authoritative advisories do not publish a clean affected range. Best-supported read is pre-1.2.0 builds using the SSE transport under spec v2024-11-05; beta-era builds are explicitly implicated in the advisory text. |
| Fixed version | Fixed in mcp-toolbox v1.2.0, where the release notes include Remove hardcoded * allowed origin for sse (releases, CHANGELOG). |
| Exposure and scanning context | This specific CVE is hard to fingerprint remotely, but the broader exposure class is real: Censys reported 12,520 Internet-accessible MCP services across 8,758 IPs as of 2026-04-28, with 1,056 data-access services observed (Censys). |
| Default posture amplifier | Toolbox docs say --allowed-origins and --allowed-hosts default to *, and warn this is insecure for production. That matters because this bug specifically nullified the intended origin restriction on SSE (CLI docs, deployment docs). |
| Disclosure and reporter | Disclosed 2026-05-27 via CVE/NVD. The security issue was opened by Deeven-Seru on 2026-04-14 and fixed by Yuan325 in PR #3054 merged 2026-04-16. |
noisgate verdict.
The decisive downward pressure is the attacker path requirement: this is a browser-mediated attack against a running Toolbox instance using a specific transport, not a clean unauthenticated service exploit you can spray across the internet. The impact can be serious on the wrong host, but the reachable population is narrow and the blast radius is usually bounded to one developer workstation or one explicitly exposed MCP service.
Why this verdict
- Browser-required chain: exploitation needs a victim to visit attacker content while Toolbox is running and reachable from the browser; that is materially narrower than a raw unauthenticated network bug.
- Transport-specific exposure: this only hits the SSE/HTTP path under MCP spec
v2024-11-05;--stdiousers and non-SSE flows are outside the blast radius. - Population limiter: MCP Toolbox is not a universal enterprise agent. Even among adopters, only a subset expose remote HTTP listeners with sensitive backends attached, so the vulnerable population is much smaller than the vendor-style score implies.
Why not higher?
I am not calling this HIGH or CRITICAL because the attacker does not simply point a socket at an exposed daemon and win. They need user interaction, a compatible browser path, a running vulnerable instance, and the right transport mode. Those are compounding friction points that sharply reduce exploit scale in real fleets.
Why not lower?
I am not dropping it to LOW because when the chain works, the attacker can pivot through a trusted MCP bridge into real data systems. On developer endpoints or analyst jump boxes with broad DB access, the confidentiality impact can be substantial even if the exploit path is finicky.
What to do — in priority order.
- Prefer
--stdiowhere possible — Move local IDE and agent workflows off remote HTTP/SSE and onto stdio where the protocol design allows it. That removes the browser-reachable attack surface entirely; for a MEDIUM finding there is no mitigation SLA, so do this during normal architecture hardening rather than as an emergency change. - Lock down
--allowed-hostsand--allowed-origins— Set explicit hostnames and frontend origins instead of*, and verify they survive startup wrappers, containers, and service definitions. Even though this bug bypassed origin checks on the vulnerable SSE path, strict host validation still cuts off common rebinding routes; there is no mitigation SLA, so apply in the next normal change window. - Keep Toolbox on loopback unless there is a real reason not to — The CLI default listen address is
127.0.0.1; keep it there for workstation use and avoid0.0.0.0unless you intentionally need shared access. That sharply shrinks the exposed population; for MEDIUM, treat this as routine hardening with no mitigation SLA. - Reduce backend privileges behind Toolbox — The real damage comes from what the configured tools can reach, so use read-only DB identities where practical and split privileged write paths away from developer-facing MCP servers. This limits blast radius even if the browser pivot succeeds; again, no mitigation SLA applies.
- Simply setting
--allowed-originson a vulnerable build does not solve the SSE path, because the issue exists specifically because that policy was overridden by a hardcoded wildcard header. - Relying on 'it's only localhost' is not enough; the product's own CLI docs warn that wildcard host settings are unsafe even for local use because browsers can be tricked into talking to loopback services.
- Version-only network scanning is weak here. You also need to know whether the instance is actually using the vulnerable HTTP/SSE path.
Crowdsourced verification payload.
Run this on the target host or developer workstation where toolbox is installed. Invoke it as python3 check_cve_2026_9739.py --binary /usr/local/bin/toolbox --config /path/to/tools.yaml; no admin rights are required unless your config files are unreadable to the current user. It checks the local Toolbox version and, if you provide config files, looks for obvious SSE transport markers before returning VULNERABLE, PATCHED, or UNKNOWN.
#!/usr/bin/env python3
# check_cve_2026_9739.py
# Detect likely exposure to CVE-2026-9739 in Google MCP Toolbox for Databases.
# Exit codes:
# 0 = PATCHED
# 1 = VULNERABLE
# 2 = UNKNOWN
import argparse
import os
import re
import shutil
import subprocess
import sys
from typing import Optional, Tuple
PATCHED_VERSION = (1, 2, 0)
VERSION_RE = re.compile(r'(\d+)\.(\d+)\.(\d+)')
SSE_PATTERNS = [
re.compile(r'\bsse\b', re.IGNORECASE),
re.compile(r'/mcp\b', re.IGNORECASE),
re.compile(r'event-stream', re.IGNORECASE),
]
def parse_version(text: str) -> Optional[Tuple[int, int, int]]:
m = VERSION_RE.search(text or '')
if not m:
return None
return tuple(int(x) for x in m.groups())
def version_str(v: Tuple[int, int, int]) -> str:
return '.'.join(str(x) for x in v)
def run_version(binary: str) -> Tuple[Optional[Tuple[int, int, int]], str]:
cmds = [
[binary, '--version'],
[binary, '-v'],
[binary, 'version'],
]
output = ''
for cmd in cmds:
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
output = (proc.stdout or '') + '\n' + (proc.stderr or '')
v = parse_version(output)
if v:
return v, output.strip()
except Exception:
continue
return None, output.strip()
def file_contains_sse(path: str) -> Optional[bool]:
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as fh:
data = fh.read()
except Exception:
return None
for pat in SSE_PATTERNS:
if pat.search(data):
return True
return False
def main() -> int:
parser = argparse.ArgumentParser(description='Check likely exposure to CVE-2026-9739')
parser.add_argument('--binary', default='toolbox', help='Path to toolbox binary (default: toolbox in PATH)')
parser.add_argument('--config', action='append', default=[], help='Optional config file(s) to inspect for SSE usage')
args = parser.parse_args()
binary = args.binary
if os.path.sep not in binary:
resolved = shutil.which(binary)
if resolved:
binary = resolved
if not os.path.exists(binary):
print('UNKNOWN - toolbox binary not found: {}'.format(args.binary))
return 2
version, raw = run_version(binary)
if not version:
print('UNKNOWN - unable to determine toolbox version from: {}'.format(raw or 'no output'))
return 2
if version >= PATCHED_VERSION:
print('PATCHED - toolbox {} is >= {}'.format(version_str(version), version_str(PATCHED_VERSION)))
return 0
# Pre-1.2.0 is potentially affected, but the CVE is specific to SSE transport.
if not args.config:
print('UNKNOWN - toolbox {} is < {}, but SSE usage was not checked (supply --config)'.format(
version_str(version), version_str(PATCHED_VERSION)
))
return 2
saw_readable = False
sse_hit = False
unreadable = []
clean = []
for cfg in args.config:
result = file_contains_sse(cfg)
if result is None:
unreadable.append(cfg)
continue
saw_readable = True
if result:
sse_hit = True
else:
clean.append(cfg)
if sse_hit:
print('VULNERABLE - toolbox {} is < {} and at least one config appears to reference SSE'.format(
version_str(version), version_str(PATCHED_VERSION)
))
return 1
if saw_readable:
print('UNKNOWN - toolbox {} is < {}, but provided config(s) did not show obvious SSE markers'.format(
version_str(version), version_str(PATCHED_VERSION)
))
return 2
print('UNKNOWN - toolbox {} is < {}, but config files were unreadable: {}'.format(
version_str(version), version_str(PATCHED_VERSION), ', '.join(unreadable) if unreadable else 'none'
))
return 2
if __name__ == '__main__':
sys.exit(main())
If you remember one thing.
mcp-toolbox instance below 1.2.0 and separate stdio-only usage from HTTP/SSE-reachable usage. For this MEDIUM call there is noisgate mitigation SLA: no mitigation SLA — go straight to the 365-day remediation window; still, use the next normal change window to lock down --allowed-hosts, --allowed-origins, and loopback-only binding on any browser-reachable instances. Then patch remaining vulnerable installs to v1.2.0 or later within the noisgate remediation SLA of ≤365 days, with developer workstations and any intentionally exposed MCP services first in line.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.