Back to Blog
high SEVERITY7 min read

CVE-2026-54290: hono cors() Reflects Any Origin With Credentials

The `hono` CORS middleware, as resolved in this service at 4.12.8, reflected the caller's `Origin` header back in `Access-Control-Allow-Origin` while also emitting `Access-Control-Allow-Credentials: true` whenever the `origin` option was left at its `'*'` default. That combination makes any website a trusted origin for credentialed cross-origin reads. The dependency range was raised from `^4.7.1` to `^4.13.5`, moving the installed copy from 4.12.8 to 4.13.5.

O
By Orbis AppSec
•Published September 26, 2026•Reviewed September 26, 2026

Answer Summary

The vulnerability affects the `hono` npm package's `cors()` middleware from `hono/cors` — this service resolved 4.12.8, and the exact affected range for CVE-2026-54290 is not published in the data available here. When `cors()` is used with `credentials: true` (or any config that leaves `origin` at its `'*'` default), the middleware echoes the request's `Origin` header into `Access-Control-Allow-Origin` alongside `Access-Control-Allow-Credentials: true`, letting an attacker-controlled page read authenticated API responses — session-bearing profile data, tokens, or registry records — straight out of a logged-in victim's browser. The fix is a dependency upgrade: the manifest range moved from `^4.7.1` to `^4.13.5`, pulling 4.13.5 in place of 4.12.8 (the PR notes 4.12.25 as the patch on the 4.12 line). No CWE identifier was assigned in the advisory data available for this finding.

Vulnerability at a Glance

cweN/A
fixUpgraded `hono` from the resolved 4.12.8 to 4.13.5 by raising the manifest range to `^4.13.5`
riskAny website can read authenticated responses from the API in a logged-in victim's browser
languageTypeScript / JavaScript (Node.js)
root cause`cors()` reflected the request `Origin` instead of refusing wildcard-plus-credentials when `origin` was left at its `'*'` default
vulnerabilityCORS origin reflection with credentials (permissive cross-origin policy)

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.

  1. A maintainer is signed in to the API in their browser; the session cookie is present.
  2. They load an unrelated page the attacker controls, or any page with an attacker-influenced script.
  3. 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
  1. The preflight for a non-simple request also passes: allowMethods defaults include POST, PUT, PATCH, and DELETE, 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 affected hono versions is equivalent to "allow every origin to read authenticated responses" — the '*' default for origin was 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: true changes 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 explicit origin allowlist.
  • Pinning only the lockfile would have left ^4.7.1 free to re-resolve below the patch; raising the manifest floor to ^4.13.5 is 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 Origin request header, read by the cors() middleware from hono/cors during request and preflight handling.
  • Sink: the Access-Control-Allow-Origin response header, emitted together with Access-Control-Allow-Credentials: true, in the code path where the origin option 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 hono dependency range was raised from ^4.7.1 to ^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.

Prevention and further reading

Frequently Asked Questions

Does upgrading to hono 4.13.5 break an app that used `cors({ credentials: true })` with no `origin` option?

It can. After the fix, wildcard-plus-credentials no longer produces a reflected origin, so credentialed cross-origin requests that silently "worked" will start failing the browser's CORS check. Set `origin` to an explicit string, array, or callback.

Is a service safe if it already passes an explicit allowlist to `cors()`?

Yes for this issue — CVE-2026-54290 is specific to the code path where `origin` falls back to `'*'`. An explicit `origin: ['https://app.example']` or an origin function was never reflected blindly, though upgrading is still recommended.

The PR title mentions hono 4.12.25 but the manifest now requests `^4.13.5` — which version is actually required?

4.12.25 is cited as the patch on the 4.12 line; this change instead raised the range to `^4.13.5`, which resolved 4.13.5 and also contains the fix. Either is acceptable; staying on `^4.13.5` avoids drifting back below the patch.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #34

Related Articles

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

SchedulePush.disableReminder Missing Authorization Check in push.js

The `disableReminder` method in the `SchedulePush` class allowed any user to disable push notification reminders for arbitrary user IDs by manipulating the `e.user_id` event parameter. The fix adds a `checkFriend()` authorization gate that verifies the requesting user has a valid friendship relationship with the bot before modifying subscription state.

critical

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

high

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.

high

How Sandboxed Iframe Popup Restriction Bypass happens in Electron and how to fix it

A high-severity flaw in Electron (CVE-2026-70608) allowed sandboxed iframes to bypass the `allow-popups` sandbox restriction through the internal OpenURL navigation path, letting malicious or compromised embedded content spawn unauthorized popup windows. The fix upgrades Electron from 40.10.6 to 41.10.3 (also patched in 42.0.1 and 39.8.10), closing the navigation-layer gap without requiring any application code changes.

high

innerHTML Injection in postAlert(): Glitch.me Data Renders Unsanitized

The `postAlert()` function fetched alert data from a Glitch.me endpoint and injected it directly into the DOM using `innerHTML`, enabling arbitrary JavaScript execution if that external source was compromised. The fix replaces the HTML string concatenation with safe DOM API methods: `document.createTextNode()` for content and `addEventListener()` for event handlers, eliminating the injection vector entirely.