A wildcard default that quietly became "reflect everything"
cors() from hono/cors is one of the first middlewares most Hono services mount. Its signature is forgiving by design: every option has a default, and origin defaults to '*'. CVE-2026-54290 is about what happened when that default met the credentials: true option.
The browser spec forbids the combination Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — a wildcard is never a valid answer for a credentialed request. Rather than treating that as a configuration error, the affected versions of the middleware took the accommodating route: it reflected whatever value arrived in the request's Origin header into Access-Control-Allow-Origin, and still sent Access-Control-Allow-Credentials: true. The response is now spec-compliant and the request succeeds. It also means every origin on the internet is an allowed origin, including https://evil.example.
This service resolved hono at 4.12.8 behind a ^4.7.1 range, with the HTTP layer served through @hono/node-server. The dependency range has been raised to ^4.13.5.
Affected Versions
| Affected | unknown (this service resolved hono 4.12.8, which is vulnerable) |
| Fixed in | unknown (upgraded here to 4.13.5; the upstream PR cites 4.12.25 as the patch on the 4.12 line) |
| Ecosystem | npm |
| CVE / GHSA | CVE-2026-54290 / not assigned |
| CWE | unknown |
The published advisory data available for this finding does not include a machine-readable affected range or a single canonical fixed version, so both rows are marked unknown rather than guessed. What is concrete: the installed copy was 4.12.8 and is now 4.13.5.
The Vulnerability Explained
The dangerous configuration is short enough to miss in review:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono()
// `origin` is omitted, so it falls back to '*'
app.use('/api/*', cors({ credentials: true }))
On the surface this reads as "allow any origin, and allow cookies." In the affected versions the runtime behaviour is worse than that reading. For a request carrying Origin: https://evil.example, the middleware resolves the allowed origin from the wildcard default, notices that credentials is enabled, and substitutes the incoming Origin value so the response validates in the browser. The headers that come back look like a deliberate, tight allowlist:
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true
Vary: Origin
The single problematic decision is the fallback: when origin is '*' and credentials is true, reflect the request's Origin instead of failing closed. There is no allowlist, no suffix check, no null-origin handling — the attacker supplies the value that is echoed back as trusted.
Attack scenario against this code path
Assume an API mounted under /api/* with cors({ credentials: true }) and session state in a cookie — a registry service that returns agent identities, key material metadata, or account records for the authenticated caller.
- A maintainer is signed in to the API in their browser; the session cookie is present.
- They load an unrelated page the attacker controls, or any page with an attacker-influenced script.
- That page runs:
const r = await fetch('https://registry.example/api/agents/me', {
credentials: 'include'
})
console.log(await r.json()) // readable: the CORS response said this origin is allowed
- The preflight for a non-simple request also passes:
allowMethodsdefaults includePOST,PUT,PATCH, andDELETE, and the preflight response carries the same reflected origin.
Because Access-Control-Allow-Credentials: true accompanies the reflected origin, the browser attaches the victim's cookie and hands the response body to the attacker's script. This is not a blind CSRF-style write — it is a full authenticated read primitive from any page the victim visits. Anything the session can fetch (profiles, tokens, registry entries, internal listings) becomes exfiltratable, and state-changing endpoints become callable with the response visible for chaining.
The real-world sting is how invisible it is in configuration review. cors({ credentials: true }) does not contain the string *, so grepping for wildcard CORS finds nothing. The wildcard lives in the middleware's defaults.
The Fix
The change in this repository is a dependency upgrade — the vulnerable logic is inside the middleware, not in application code. The manifest's dependency range was raised:
- "hono": "^4.7.1"
+ "hono": "^4.13.5"
The lockfile's resolved hono entry moved accordingly, from 4.12.8 to 4.13.5 (metadata for the new entry was refreshed as part of the install; the version numbers above are the substantive change).
Why the range bump and not just a lockfile pin? The old range ^4.7.1 happily re-resolves to any 4.x release, including versions below the patch, on a fresh npm install or in a CI cache miss. Raising the floor to ^4.13.5 makes the fixed behaviour the minimum the dependency solver may choose, so the vulnerability cannot silently reappear the next time the lockfile is regenerated.
In the patched middleware, leaving origin at '*' no longer produces a reflected origin for credentialed requests — the wildcard-plus-credentials combination stops being papered over. That is a behavioural change you should plan for: any credentialed cross-origin call that only worked because of the reflection will now be rejected by the browser. The correct follow-up in application code is to state the policy explicitly rather than relying on defaults:
app.use('/api/*', cors({
origin: ['https://app.example', 'https://admin.example'],
credentials: true
}))
If origins are dynamic, pass a function to origin and validate the incoming value against a known set — never return the input unconditionally, which reimplements the bug in userland.
Key Takeaways
cors({ credentials: true })in affectedhonoversions is equivalent to "allow every origin to read authenticated responses" — the'*'default fororiginwas silently converted into origin reflection instead of being rejected.- A permissive CORS policy is not always spelled
*in your source. Auditing for wildcard CORS by searching for the literal*misses the case where the wildcard is a library default. Access-Control-Allow-Credentials: truechanges the blast radius of a CORS mistake from "attacker can make anonymous requests" to "attacker can read the victim's authenticated responses"; always pair it with an explicitoriginallowlist.- Pinning only the lockfile would have left
^4.7.1free to re-resolve below the patch; raising the manifest floor to^4.13.5is what makes the fix durable across reinstalls. - After upgrading, expect credentialed cross-origin requests that relied on the old reflection to start failing — that failure is the fix working, and the remedy is configuration, not a downgrade.
How Orbis AppSec Detected This
- Source: the client-supplied
Originrequest header, read by thecors()middleware fromhono/corsduring request and preflight handling. - Sink: the
Access-Control-Allow-Originresponse header, emitted together withAccess-Control-Allow-Credentials: true, in the code path where theoriginoption falls back to its'*'default. - Missing control: no allowlist validation of the incoming
Origin, and no refusal of the invalid wildcard-plus-credentials combination — the attacker-supplied value was echoed back as an approved origin. - CWE: unknown — no CWE identifier was assigned in the advisory data available for CVE-2026-54290.
- Fix: the
honodependency range was raised from^4.7.1to^4.13.5, replacing the resolved 4.12.8 with 4.13.5, which no longer reflects arbitrary origins for credentialed requests.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-54290 is a reminder that a framework's friendliest defaults can be its sharpest edge. cors() tried to make an invalid configuration work — wildcard origin plus credentials — and in doing so turned a permissive policy into a reflect-any-origin policy that browsers happily honour with cookies attached. For a service exposing authenticated registry data, that is a direct read primitive for any page a signed-in user visits.
The remediation here was mechanical: move hono off 4.12.8 and raise the floor of the dependency range to ^4.13.5 so it stays there. The durable lesson is in the configuration: pass an explicit origin allowlist alongside credentials: true, and treat "no origin specified" on a credentialed CORS middleware as a finding in its own right. Details of the advisory are on the CVE-2026-54290 record.