Skip to content

Security and reliability fixes for jetstream - #5836

Merged
norman-abramovitz merged 9 commits into
cloudfoundry:developfrom
nabramovitz:security/review-fixes
Aug 23, 2026
Merged

Security and reliability fixes for jetstream#5836
norman-abramovitz merged 9 commits into
cloudfoundry:developfrom
nabramovitz:security/review-fixes

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

Several security and reliability issues in the jetstream backend, found while reviewing the current develop. Each is a separate commit.

Security

  • Stop logging the OAuth client secret and the app-SSH one-time code. RefreshOidcToken logged the token metadata (including clientSecret) and getSSHCode dumped the full authorize response — whose Location header carries the one-time SSH code — at Info level. Keeps the non-sensitive fields; the secret-bearing lines are left commented out.
  • Warn when ENCRYPTION_KEY is the well-known default from config.example. getEncryptionKey used the configured key verbatim, so an unmodified packaged deploy encrypts the token store with a publicly known key. Detected at startup and warned (deploys are unaffected); the warning also notes that changing the key later makes existing encrypted data unreadable.
  • Hash API key secrets at rest. Secrets were stored and matched in plaintext, so a database dump yielded usable long-lived credentials. Now stores and looks up a SHA-256 hash (fits the existing VARCHAR(64) column; a 384-bit random secret needs no salt). A one-time migration hashes existing rows so already-issued keys keep working.
  • Set SameSite=Lax on the session cookie (previously unset, relying on the browser default).
  • Validate Origin on WebSocket upgrades. The upgrader accepted any Origin (InsecureSkipVerify), leaving app SSH and log-stream endpoints open to cross-site WebSocket hijacking. Now enforces same-origin plus the hosts configured in ALLOWED_ORIGINS, via the library's OriginPatterns.

Reliability

  • Don't log.Fatal on an SSO-login info-fetch failure. A transient CF outage during one user's login exited the whole process for every user; now logs and returns.
  • Buffer the proxy fan-out channel. On the timeout paths the per-endpoint goroutines blocked forever on an unbuffered send, leaking goroutines and their buffered response bodies; long-running operations past 30s leaked deterministically.

Backend test suite passes; fixes with logic carry a test (API-key hash lookup, WebSocket origin allow-list).

RefreshOidcToken logged the token metadata JSON, the parsed metadata
struct, and the OAuth client secret at Info level on every OIDC token
refresh; getSSHCode dumped the full authorize response, whose Location
header carries the one-time SSH auth code. Keep the non-sensitive
fields (client ID, response status), state what is withheld, and leave
the secret-bearing lines commented out.
getEncryptionKey used the configured key verbatim with no check, so an
unmodified deploy of the packaged config.properties encrypts the token
store with the well-known key from config.example. Detect that value at
startup and log a warning, mirroring the existing default-session-secret
check. Deploys are unaffected (warn and continue).
API key secrets were stored verbatim in api_keys.secret and matched by
plaintext SQL equality, so a database dump yielded usable long-lived
credentials for every user. Store and look up a SHA-256 hash instead
(the 384-bit random secret needs no salt/bcrypt, and the 64-char hash
fits the existing VARCHAR(64) column). The plaintext is still returned
once at creation. A one-time migration hashes existing rows in place, so
keys already issued keep working.
DoLoginToCNSIwithConsoleUAAtoken called log.Fatal (os.Exit(1)) when the
CF info fetch failed during SSO auto-connect, so a transient CF outage at
the moment one user logged in exited jetstream for every user. Log the
error and return it, as every other branch in the function already does.
ProxyRequest and DoProxyRequest launched one doRequest goroutine per
endpoint sending on an unbuffered channel. On the timeout paths (and on
an early build-error return) the function returns without draining, so
each goroutine blocked forever on the send, holding its full response
body. Long-running ops past 30s leaked deterministically. Buffer the
channel to the goroutine count so every send completes and the goroutine
exits.
The session cookie set Secure and HttpOnly but never SameSite, so it was
emitted with the browser default and cross-site protection relied on each
browser's behaviour. Set SameSite=Lax on all three session stores
(postgres/mysql/sqlite) as defence-in-depth alongside the XSRF token; Lax
keeps the cookie on top-level navigations such as the SSO login redirect.
The WebSocket upgrader accepted any Origin (CheckOrigin always returned
true), leaving app SSH and log-stream endpoints open to Cross-Site
WebSocket Hijacking. Enforce an allow-list: same-origin requests and
origins listed in ALLOWED_ORIGINS are accepted (empty Origin, i.e.
non-browser clients, is allowed); everything else is rejected. The
allow-list is populated at startup from the same config CORS uses.
Comment thread src/jetstream/crypto/crypto.go Fixed
Switch API key secret hashing from SHA-256 to HMAC-SHA256 keyed with the
server encryption key. A database dump alone can no longer verify guessed
secrets, and it clears the CodeQL weak-hashing alert that flags a bare
fast hash on secret-like data (an API key is a 384-bit random token, so
the fast hash was already appropriate; the pepper makes that explicit and
adds defence in depth). The hash stays deterministic, preserving the
indexed exact-match lookup. Note: the hash is now tied to the encryption
key, so changing that key invalidates stored API keys.
@nabramovitz

Copy link
Copy Markdown
Contributor Author

Addressed the CodeQL weak-sensitive-data-hashing finding on the API key hashing.

The alert fired because a fast hash (SHA-256) was applied to secret-like data — the rule assumes a low-entropy password, where a slow hash (bcrypt/argon2) is needed. An API key here is a 384-bit random token, so a fast hash was already appropriate and a slow one would only cost the O(1) indexed WHERE secret = $1 lookup for no real gain against brute force.

Rather than dismiss it, I switched to HMAC-SHA256 keyed with the server encryption key (a pepper). This clears the alert and is a genuine improvement: a database dump alone can no longer verify guessed secrets. It stays deterministic, so the indexed lookup is preserved, and the one-time migration hashes existing rows in place so already-issued keys keep working.

One operator-facing consequence, noted in a [Breaking Changes] changelog fragment: the hash is now tied to ENCRYPTION_KEY, so changing that key invalidates stored API keys — the same way it already invalidates stored endpoint tokens.

Verified locally with the same default query suite CI uses: the alert is gone from crypto/crypto.go; the only remaining weak-sensitive-data-hashing hit is the pre-existing one in plugins/backup/backup_restore.go, which is outside this change.

@norman-abramovitz norman-abramovitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@norman-abramovitz
norman-abramovitz merged commit 04d8ec1 into cloudfoundry:develop Aug 23, 2026
23 checks passed
@nabramovitz
nabramovitz deleted the security/review-fixes branch August 23, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants