Skip to content

[PM-43128] fix(cli): skip Host allowlist for unix-socket and fd serve transports - #23024

Open
Tyagiquamar wants to merge 3 commits into
bitwarden:mainfrom
Tyagiquamar:fix/serve-socket-transport-host-allowlist
Open

[PM-43128] fix(cli): skip Host allowlist for unix-socket and fd serve transports#23024
Tyagiquamar wants to merge 3 commits into
bitwarden:mainfrom
Tyagiquamar:fix/serve-socket-transport-host-allowlist

Conversation

@Tyagiquamar

Copy link
Copy Markdown

Fixes #22867

Problem

The Host-header allowlist from #20881 is built from --port (default 8087) before the transport is chosen, and it is applied to the unix://, fd+listening:// and fd+connected:// transports from #14262 even though none of them binds a TCP port. Every request over those transports is rejected with 403 unless the client sends Host: localhost:8087 — naming a port nothing is listening on. This silently breaks every existing unix-socket consumer on upgrade.

The allowlist defends against DNS rebinding, which requires a resolvable hostname and a reachable TCP endpoint. Socket transports have neither — browsers cannot open AF_UNIX, and inherited file descriptors are not network-reachable — so the check blocks legitimate clients while excluding no attacker.

Fix

Treat socket transports like --hostname all for the Host check: skip the allowlist. The Origin check stays enabled for these transports at no cost.

Validation

  • npx jest apps/cli/src/commands/serve.command.spec.ts — 19 passed, including a new isSocketTransport describe block covering unix://, fd+listening://, fd+connected:// (recognized) and localhost/all/unix.example/0.0.0.0 (not recognized)
  • All pre-existing middleware and allowlist tests unchanged and green

…requests

Concurrent callers of getProfileCreationDate (the account-security and
new-account nudge services during popup init) each resolved before the
cache was populated and independently called apiService.getProfile(),
producing two identical GET /accounts/profile requests on every popup
open. fetchAndCacheProfile now shares a single in-flight promise,
clearing it once settled so later calls re-fetch as before.

Fixes bitwarden#22840
The Host-header allowlist built from --port was applied to the
unix://, fd+listening:// and fd+connected:// transports even though
they bind no TCP port, so every request was rejected with 403 unless
the client sent Host: localhost:<port> naming a port nothing listens
on. The allowlist defends against DNS rebinding, which requires a
resolvable hostname and a TCP endpoint; socket transports have
neither, so the allowlist blocked legitimate clients while excluding
no attacker. The Origin check remains active for these transports.
@Tyagiquamar
Tyagiquamar requested a review from a team as a code owner September 5, 2026 14:55
@bitwarden-bot

Copy link
Copy Markdown
Collaborator

Thank you for your contribution! We've added this to our internal tracking system for review.
ID: PM-43128
Link: https://bitwarden.atlassian.net/browse/PM-43128

Details on our contribution process can be found here: https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process.

@bitwarden-bot bitwarden-bot changed the title fix(cli): skip Host allowlist for unix-socket and fd serve transports [PM-43128] fix(cli): skip Host allowlist for unix-socket and fd serve transports Sep 5, 2026
@bitwarden-bot bitwarden-bot added community-pr needs-qa Marks a PR as requiring QA approval labels Sep 5, 2026
@ggiesen

ggiesen commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for picking this up. The transport check looks right to me: isSocketTransport covers
all three schemes from #14262, and skipping the allowlist there leaves rejectIfOriginPresent
active, so the CSRF guard is untouched.

Two things while this is open.

The help doesn't document the socket transports at all. The complete bw serve --help is:

--hostname <hostname>   The hostname to bind your API webserver to.
--port <port>           The port to run your API webserver on.

Notes:
  Default hostname is `localhost`.
  Use hostname `all` for no hostname binding.
  Default port is `8087`.

Examples:
  bw serve
  bw serve --port 8080
  bw serve --hostname bwapi.mydomain.com --port 80

Nothing there suggests --hostname takes anything other than a hostname. unix://,
fd+listening:// and fd+connected:// appear in exactly one file in the repository,
apps/cli/src/commands/serve.command.ts, which is the implementation. They are in no help
text, no example and no doc: a code search for fd+listening returns that single file. So
this PR is fixing a 403 in a feature the CLI never tells anyone exists, which is likely part
of why it went unreported between #14262 and now.

The same gap covers the allowlist itself. "Use hostname all for no hostname binding"
describes the bind, not the allowlist, and the allowlist is the part people need to know
about. Everyone who hits this arrives from an opaque 403 whose only explanation sits in the
daemon's own log. It has now been rediscovered independently at least twice (this issue, and
the container case in external-secrets/external-secrets#6850), and both times the explanation
had to be written outside this repo.

Since the PR already enumerates the schemes in SOCKET_TRANSPORT_SCHEMES, folding the help
into the same change would close both gaps:

  • An --hostname description noting it also accepts unix:///path/to.sock,
    fd+listening://<fd> and fd+connected://<fd>, plus an example line.
  • A Notes line stating that requests are rejected unless the Host header matches the bound
    hostname and port, and that all and the socket transports skip that check.
  • --disable-origin-protection's description is now incomplete. Since fix(cli): harden bw serve origin-protection against DNS rebinding [PM-36600] #20881 it disables the
    Host allowlist as well as the Origin check, but it still reads "allows requests with origin
    header".
  • The bw serve --hostname bwapi.mydomain.com --port 80 example only works if clients send
    exactly that Host. Behind a reverse proxy or a container service name it returns 403, which
    is a common deployment shape.

Unrelated changes. The diff also carries
libs/angular/src/vault/services/vault-profile.service.ts and its spec, an in-flight
getProfile dedup that is unrelated to the CLI Host allowlist. Probably a branch artifact;
splitting it out would keep this reviewable.

@Tyagiquamar

Copy link
Copy Markdown
Author

Thanks, both points are fair.

The help text gap is closed in 7b88300: the --hostname description now lists the unix://, fd+listening:// and fd+connected:// transports, the Notes section states that requests are rejected unless the Host header matches the bound hostname and port (skipped for all and the socket/fd transports), --disable-origin-protection's description reflects that it skips the Host allowlist as well, and there is a socket example under Examples.

On the unrelated changes: the vault-profile.service.ts change belongs to #23023 (PM-43127). This branch is stacked on top of that one, which is why it appears in the diff here; the vault-profile commit is identical to the one under review in #23023.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-pr needs-qa Marks a PR as requiring QA approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bw serve on a unix socket rejects every request with 403 unless the client sends a port the daemon never binds

3 participants