A Missing await, a Default Allow: This Week's Fail-Open Auth Bugs
Unleash, deepstream and 9router each shipped an auth check that silently let requests through. Plus an MCP path traversal and a CVSS 10 SunEditor XSS.
Three unrelated projects shipped the same kind of bug this week: an authorization check that was in the code and did nothing. Unleash forgot an await, deepstream allowed any action its rules table didn't list, and 9router trusted a header the client controls. None of them crashed or logged an error. The requests just went through.
If you run any of these, expose an MCP server that writes files, or render user HTML from SunEditor, there is an upgrade below.
What shipped
Unleash: a missing await on a permission check
- Package:
unleash-server, affected< 8.0.3, fixed in8.0.3 - Advisory: CVE-2026-77426 / GHSA-72h8-wp98-7hch, high
The handler for POST /api/admin/segments/strategies called the async hasPermission() without await. A pending Promise is truthy, so the if (!hasPermission) guard never fired. Per the advisory, any authenticated user could change segment assignments on any strategy in any project. The same advisory lists four cross-project issues where handlers never checked that the feature or strategy belonged to the project in the URL.
npm install unleash-server@^8.0.3
deepstream: an unmapped action defaults to allow
- Package:
@deepstream/server, affected10.1.0, fixed in10.1.1 - Advisory: CVE-2026-63116 / GHSA-89vx-jh4q-vg3w, high, CVSS 8.8
The Valve permission system had no rule entry for the PATCH_MULTI record action. The rule lookup returned null, and null was treated as allowed, so any authenticated user could overwrite any record. Only the config (Valve) permission type is affected, which the advisory notes is the recommended production setup.
npm install @deepstream/server@10.1.1
9router: four advisories in an LLM router
- Package:
9router, upgrade to0.5.8or later, which covers all four - Advisories: CVE-2026-56681, CVE-2026-56675, CVE-2026-56676, CVE-2026-56679, all high
9router exempted "local" requests from its API key and decided what was local in two broken ways: by reading X-9r-Real-Ip, a header any client can send, and by trusting loopback traffic, which behind a same-host nginx proxy includes public requests. Either route let a remote caller spend the owner's configured LLM provider access. The same window fixed a DNS-rebinding SSRF in image prefetching and a mass-assignment bug that let an authenticated user switch off login entirely by sending requireLogin: false to PATCH /api/settings.
npm install 9router@^0.5.8
notebooklm-mcp: path traversal through an MCP tool
- Package:
@roomi-fields/notebooklm-mcp, affected>= 1.6.0, < 2.0.3, fixed in2.0.3 - Advisory: CVE-2026-61647 / GHSA-jjhp-8crj-mppq, high
The vault_batch tool passed a caller-supplied vault_dir straight to path.resolve() and fs.mkdir(), with no check that the result stayed inside the vault. The caller of an MCP tool is usually a model, and the advisory explicitly counts a prompt-injected LLM as the attacker: one that has read untrusted content can be steered into writing files anywhere the server process can.
npm install @roomi-fields/notebooklm-mcp@^2.0.3
SunEditor: sanitizer bypass, CVSS 10.0
- Package:
suneditor, affected<= 2.47.10, fixed in2.47.11 - Advisory: CVE-2026-59167 / GHSA-6rf4-v2fh-m6p4, critical
The sanitizer left event-handler attributes on namespaced tags such as <a:b onclick=…>. If your app stores editor HTML and shows it to other users, that is stored XSS.
npm install suneditor@^2.47.11
Fail-open authorization, explained
An authorization check is fail-open when any outcome other than an explicit "yes" still lets the request through. The code reads "deny unless permitted"; the runtime does "permit unless denied".
The three auth bugs above are three ways to get there:
- The check returns a Promise. A pending Promise is an object, and objects are truthy, so
!promiseis alwaysfalse. - The check has no answer. A lookup misses a case, returns
null, and the code reads "no rule" as "no restriction". - The check asks the wrong party. "Is this request local?" gets answered from a header the requester wrote.
The first one is especially common in AI-generated code. A helper named hasPermission or canEdit reads like a boolean, and a model completes the call the way it most often sees boolean helpers called: without await. The result type-checks. The Unleash handler is TypeScript, and it shipped. Nothing crashes, the happy-path test passes, and the case that breaks is the one nobody tests: a user who should be refused.
Here is the same shape in a Next.js route handler:
// app/api/projects/[id]/route.ts (vulnerable)
type Ctx = { params: Promise<{ id: string }> };
export async function DELETE(req: Request, { params }: Ctx) {
const { id } = await params;
const user = await requireUser();
// canEditProject is async, so `allowed` is a Promise<boolean>
const allowed = canEditProject(user.id, id);
if (!allowed) {
return new Response("Forbidden", { status: 403 }); // never runs
}
await db.delete(projects).where(eq(projects.id, id));
return new Response(null, { status: 204 });
}
Adding await fixes this call site. Changing the helper's shape fixes the class:
// fixed: the check throws instead of returning a boolean
export async function DELETE(req: Request, { params }: Ctx) {
const { id } = await params;
const user = await requireUser();
// throws a 403 on anything but an explicit grant
await assertCanEditProject(user.id, id);
await db.delete(projects).where(eq(projects.id, id));
return new Response(null, { status: 204 });
}
An assertion has one success path. If someone forgets the await on it, the call becomes a floating Promise, which typescript-eslint's no-floating-promises rule reports as an error. Inside the assertion, make every branch that is not an explicit grant end in a denial, including the lookup that found no rule.
The review check is one question: if this check returned undefined, returned a Promise, or threw, which way would the request go? If the answer is "through", it fails open.
Check your own repo
npm ls unleash-server @deepstream/server 9router @roomi-fields/notebooklm-mcp suneditor
npm audit
npx guardvibe@3.35.0 audit . # 3.35.0 flags the affected deepstream, 9router, notebooklm-mcp and SunEditor versions
GuardVibe 3.35.0, released today, adds version rules for four of the five advisories above (VG1119, VG1121, VG1122, VG1123); the Unleash advisory is not covered yet, so check it with npm ls and the advisory's fixed version.
For the code pattern itself, turn on @typescript-eslint/no-floating-promises and @typescript-eslint/no-misused-promises in a typed ESLint config, and give every protected route one test that asserts a user without the permission gets a 403.
Sources
- GHSA-72h8-wp98-7hch: Unleash missing await on permission check and cross-project IDOR (CVE-2026-77426)
- GHSA-89vx-jh4q-vg3w: deepstream PATCH_MULTI bypasses Valve permissions (CVE-2026-63116)
- GHSA-5mj8-gf6m-fhw8: 9router authentication bypass via spoofable X-9r-Real-Ip (CVE-2026-56681)
- GHSA-x5c9-v98j-722r: 9router unauthenticated /v1 access behind a reverse proxy (CVE-2026-56675)
- GHSA-cmhj-wh2f-9cgx: 9router image prefetch DNS rebinding SSRF (CVE-2026-56676)
- GHSA-vmjq-hvgq-2wv4: 9router mass assignment in PATCH /api/settings (CVE-2026-56679)
- GHSA-jjhp-8crj-mppq: notebooklm-mcp path traversal in vault_batch (CVE-2026-61647)
- GHSA-6rf4-v2fh-m6p4: SunEditor XSS sanitizer bypass (CVE-2026-59167)
- typescript-eslint: no-floating-promises
- typescript-eslint: no-misused-promises