Propagate HTTP Basic Auth userinfo through the service proxy - #448
Open
mkitti wants to merge 1 commit into
Open
Conversation
A published service_url may carry userinfo (http://user:pass@node:port/)
for a service that enforces its own HTTP Basic Auth rather than a
query-string token. upstream_from_service_url previously rejected any
such URL outright -- its _UPSTREAM_RE regex requires a bare host:port
netloc, so userinfo failed the shape check and the resolve endpoint
403'd (surfaced to users as nginx's own 503 page), even though the
backend was healthy and reachable. build_proxied_service_url separately
discarded any userinfo when rewriting to the browser-facing HTTPS URL,
so even a permissive upstream check wouldn't have helped the credential
reach the browser.
- upstream_from_service_url: split userinfo off the netloc first
(_split_userinfo) and discard it unconditionally before the existing
_UPSTREAM_RE shape check runs against the host:port remainder only.
Userinfo is never validated here -- it's inert once discarded, and
every existing SSRF/injection guarantee (safe-host check, zone/network
allowlists, the strict regex) still applies to exactly the same string
as before.
- build_proxied_service_url: extract userinfo the same way and reattach
it verbatim to the rewritten netloc (no split into username/password,
no percent-decode/re-encode -- treated like path/query/fragment
already are). Adds one narrow check, _is_safe_userinfo, rejecting
control characters -- this function's output can end up in a browser
context, unlike the discarded value in upstream_from_service_url. On a
bad userinfo, drops just the credential and still returns the rest of
the URL rather than refusing to publish the job's link at all.
Discovered via JaneliaSciComp/janelia-mojo-sandbox, whose ttyd-backed
classroom terminal published exactly this shape of URL and got a
confusing 503 despite the backend being fully healthy.
Tests: tests/test_service_proxy_urls.py gains userinfo-stripping and
control-character cases for both functions; tests/test_service_proxy.py
gains an end-to-end roundtrip proving nginx's X-Fg-Upstream never
carries credentials while the job-detail response does. All existing
cases pass unchanged except the one flipped case (userinfo used to be
an explicit rejection case, now resolves).
docs/ServiceProxy.md: documents userinfo as a supported alternative to
${FG_SERVICE_TOKEN} in Residual risks, with its browser-handling caveats
relative to a query-string token.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
upstream_from_service_urlpreviously rejected any publishedservice_urlcontaining HTTP Basic Auth userinfo (http://user:pass@node:port/) outright, since_UPSTREAM_RErequires a barehost:portnetloc -- the resolve endpoint would 403 (surfaced as nginx's own 503 page) even though the backend was healthy and reachable.build_proxied_service_urlseparately discarded any userinfo when rewriting to the browser-facing HTTPS URL, so even a permissive upstream check wouldn't have let the credential reach the browser.service_urlcarry userinfo end to end: stripped before it ever reaches nginx'sproxy_passtarget (which must never see credentials), and forwarded verbatim to the final browser-facing URL so opening the link auto-authenticates via the browser's native Basic-Auth-from-URL behavior -- the same UX apps using${FG_SERVICE_TOKEN}as a query-string token already get.Discovered via
JaneliaSciComp/janelia-mojo-sandbox, whose ttyd-backed classroom terminal publishes exactly this shape of URL and hit a confusing 503 despite the backend being fully healthy the whole time.What's in it
upstream_from_service_url: splits userinfo off the netloc (_split_userinfo) and discards it unconditionally before the existing_UPSTREAM_REshape check runs against thehost:portremainder only. Userinfo is never validated here -- it's inert once discarded -- and every existing SSRF/injection guarantee (safe-host check, zone/network allowlists, the strict regex) still applies to exactly the same string as before.build_proxied_service_url: extracts userinfo the same way and reattaches it verbatim to the rewritten netloc (no split into username/password, no percent-decode/re-encode -- treated like path/query/fragment already are). Adds one narrow check,_is_safe_userinfo, rejecting control characters, since this function's output can end up in a browser context unlike the discarded value inupstream_from_service_url. On a bad userinfo, drops just the credential and still returns the rest of the URL rather than refusing to publish the job's link at all.docs/ServiceProxy.md: documents userinfo as a supported alternative to${FG_SERVICE_TOKEN}in Residual risks, with its browser-handling caveats relative to a query-string token.Test plan
tests/test_service_proxy_urls.py: added userinfo-stripping and control-character cases for both functions; flipped the one existing case that asserted userinfo was rejected (it now resolves). All 72 cases pass.tests/test_service_proxy.py: added an end-to-end roundtrip proving nginx'sX-Fg-Upstreamnever carries credentials while the job-detail response does. All 26 cases pass.tests/run: no regressions outside these two files (27 unrelated pre-existing failures in this environment arepytest-asyncio/frontend-build gaps, not related to this change).Known follow-up (deliberately not in this PR)
JobDB.service_urlcurrently persists the rawservice_url(including any userinfo) to the database, sinceresolve_service_upstreamneeds a fast, single indexed read and can't do a live worker RPC per proxied request. The credential riding along in that same column is incidental to the current single-string-column data model, not something the resolve path actually needs (it only ever extractshost:port). A follow-up PR could strip userinfo before the DB write inget_joband keep the raw, credential-bearing value only in that request's local scope for building the immediate browser-facing response -- so credentials would never touch the database or its backups at all.🤖 Generated with Claude Code