Keep a query-carried credential out of GraphQL introspection's failure log - #1575
Open
GeiserX wants to merge 1 commit into
Open
Keep a query-carried credential out of GraphQL introspection's failure log#1575GeiserX wants to merge 1 commit into
GeiserX wants to merge 1 commit into
Conversation
…e log query is a supported credential carrier, so an endpoint can be reached with ?token=<secret>. Introspection built its request from a URL string, and setUrl keeps a string verbatim as request.url; every HttpClientError renders method + request.url into its message getter, and the failure cause is logged raw. So a transport failure or a non-JSON response wrote the secret to the log. Build the request from a URL object instead. setUrl then moves the query into request.urlParams and clears it from request.url, so the secret is absent from the message and from anything else rendering the URL. The client recombines the two when it executes, so nothing changes on the wire. Handles the endpoint's own query string too, since a configured endpoint can carry a credential.
This was referenced Aug 13, 2026
Author
|
Context for this one: #1585 explains why this PR and twelve others exist — they came out of a single pass over credential handling, asking for each credential where it ends up, how long it stays, and who can read it once it's there. This PR stands alone and doesn't depend on any of the others. |
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.
TL;DR
A GraphQL connection whose key rides in the query string gets that key written to the process log whenever introspection fails.
queryis a supported credential carrier, so an endpoint can legitimately be reached with?token=<secret>.One-line cause: the request is built from a URL string, and
HttpClientRequest.setUrlkeeps a string verbatim asrequest.url. EveryHttpClientErrorrenders${method} ${request.url}into itsmessage, and introspection logs the raw cause.Fix: build the request from a URL object.
setUrlthen moves the query intorequest.urlParamsand clears it fromrequest.url, so the secret is gone from the message — and from anything else that renders the request URL. Nothing changes on the wire.The path, precisely
introspectfolds the query parameters into the endpoint and passes the result toHttpClientRequest.post.setUrlstores it asrequest.urlunchanged. (Passed aURL, it splitssearchParamsintourlParamsand clearssearch— that difference is the whole fix.)HttpClientError'smethodAndUrlgetter is`${this.request.method} ${this.request.url}`, andmessageis built from it.Effect.tapCause(cause => Effect.logError("graphql introspection request failed", cause))renders that throughCause.pretty, which rebuilds the first line from the livemessagegetter.Triggered by an ordinary transport failure — dead host, connection refused, TLS error — and by the sibling
DecodeErrorpath when a reachable endpoint returns non-JSON. Both reached on every credentialed introspection, via the health check and the connect-time introspect.This is a path that bypasses the plugin's own precedent: the sibling invoke path already strips the query for telemetry with
endpointForTelemetry, so the query string is treated as credential-bearing elsewhere in the same plugin.Why fix it at the request rather than by scrubbing the log
Scrubbing the log would fix one call site. Building the request correctly keeps the secret out of
request.urlentirely, so every future renderer of that URL — a new log line, a span attribute, an error surfaced to a caller — is covered without anyone having to remember.The endpoint's own query string is handled the same way, not just the separately-supplied parameters, since a configured endpoint can carry a credential too.
Tests
introspect-credential-logging.test.tsinstalls a capturing logger and asserts on what was actually written, rather than on a model of it. Three cases: a query-parameter credential, the same credential in the endpoint's own query, and the wire check.Both directions are pinned. The tests assert the secret is absent from the log and that it is still sent to the upstream — a "fix" that stopped sending the parameter would satisfy the first on its own and silently break authentication. There is also a positive control asserting the failure was logged at all, so an empty capture cannot pass.
Mutation-checked, each mutation verified to have landed, with an unmutated control before and after:
posta string again (i.e. revert this PR)Package: 100 passed / 12 files.
tsgo --noEmit,oxlint --deny-warningsandoxfmt --checkclean on the changed files.