Skip to content

auth/upstream: keep only printable ASCII, and name the cause's class - #16

Open
jeremy wants to merge 1 commit into
mainfrom
upstream-printable-ascii
Open

jeremy wants to merge 1 commit into
mainfrom
upstream-printable-ascii

Conversation

@jeremy

@jeremy jeremy commented Oct 19, 2026

Copy link
Copy Markdown
Member

auth/upstream was extracted (#10) from a basecamp-mcp-server snapshot taken before its #265 finished. Two of #265's last changes didn't come across. basecamp-mcp-server is about to delete its copy and import this package, so without them it would regress on both. This PR moves them over as they were reviewed there.

Changes

  • Endpoint text is stripped to printable ASCII before redaction and rendering. upstream.PrintableASCII replaces LogSafeRune, and both exchange.stripControls and ScrubDescription use it. LogSafeRune drops controls and format characters but keeps combining marks. So a client secret echoed with U+034F, U+FE0F or U+20DD after every third byte splits into pieces shorter than both the 8-byte fragment pass and the 20-byte token-run pass. A viewer that drops the marks then shows the whole secret. TestInvisibleMarkCannotSplitASecret fails 3 of 3 against main and passes here. OAuth error_description is ASCII by specification (RFC 6749 §5.2), so a conformant endpoint loses nothing. Non-ASCII letters are now dropped: café renders as caf, and the test case says so.
  • Failure.Error() appends a fixed phrase for its cause's class, for example deadline exceeded, connection refused, TLS certificate verification failed or unexpected JSON value for expires_in. An operator can then tell failures apart without the cause's text. Only the cause's type, sentinel identity and a JSON type error's struct-field path (our own names) are read from it. TestCauseClass checks that no class renders the cause's text.
  • upstreamtest gains the ControlSplitDescription kind. AssertClean now reads text the way a viewer that drops invisible characters would.
  • Exchange tests gain basecamp-mcp-server's U+FFFD / invalid-UTF-8 split test (TestErrorDescriptionRedactsReplacementSplitCredentials). It already passes on main and pins that behaviour.

API change

LogSafeRune is removed and PrintableASCII is added. No consumer calls LogSafeRune: fizzy-mcp-server imports auth/exchange only.

make ready passes: vet, race tests and build.

auth/upstream was extracted from basecamp-mcp-server before its #265
finished, and missed two of #265's last changes. basecamp-mcp-server is
about to adopt this package in place of its own copy, and would lose both.

- Strip endpoint text to printable ASCII before redacting and rendering it
  (upstream.PrintableASCII, replacing LogSafeRune). LogSafeRune drops
  controls and format characters but keeps combining marks: a client
  secret with U+034F or U+FE0F after every third byte splits into pieces
  shorter than both the fragment and token-run redactions, and a viewer
  that drops the marks shows the secret whole.
  TestInvisibleMarkCannotSplitASecret fails 3 of 3 against main. OAuth
  error descriptions are ASCII by specification (RFC 6749 §5.2), so a
  conformant endpoint loses nothing.
- Failure.Error() appends a fixed phrase for its cause's class (deadline
  exceeded, connection refused, TLS certificate verification failed, ...),
  so an operator can tell failures apart without the cause's text.
  Nothing is read from the cause except its type, sentinel identity, and
  a JSON type error's struct-field path.
- upstreamtest gains the control-split error_description kind, and
  AssertClean reads text as a viewer that drops invisible characters
  would.
- The exchange tests gain basecamp-mcp-server's U+FFFD/invalid-UTF-8 split
  test.
Copilot AI balanced review requested due to automatic review settings October 19, 2026 23:25
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T23:34:01.279461Z 9dffd14 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Cause labels can expose unchecked operation text, and normalized assertions can miss literal credential leaks.

2 open issues
What changed in this PR

Ports upstream error-handling hardening needed for Basecamp’s adoption of the shared exchange client.

Changes:

  • Normalizes endpoint descriptions to printable ASCII before redaction.
  • Adds cause-class labels to infrastructure errors.
  • Expands hostile-endpoint fixtures and credential-redaction tests.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
auth/​upstream/​upstreamtest/​hostile.go Adds invisible-split fixtures and normalized leak assertions.
auth/​upstream/​failure.go Adds ASCII filtering and cause classification.
auth/​upstream/​failure_test.go Tests filtering and cause labels.
auth/​exchange/​oautherror.go Shares ASCII normalization with error rendering.
auth/​exchange/​exchange_test.go Adds split-credential regression tests.

Review effort: Balanced

Comment thread auth/upstream/failure.go
case errors.As(err, &protoError):
return "malformed HTTP response"
case errors.As(err, &opErr):
return "network error during " + opErr.Op
// credential threaded with any of them still counts as present.
func AssertClean(t testing.TB, sink, text string, secrets ...string) {
t.Helper()
text = strings.Map(func(r rune) rune {

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9dffd14205

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread auth/upstream/failure.go
Comment on lines +179 to +180
case errors.As(err, &opErr):
return "network error during " + opErr.Op

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not render arbitrary net.OpError.Op values

When a supplied HTTPClient/transport returns a *net.OpError, Op is an exported, unconstrained string rather than a type-derived constant, so concatenating it directly breaks Failure.Error()'s safe-for-any-sink guarantee and can expose credentials or inject control characters. Keep this class entirely fixed or map Op through an explicit allowlist instead of rendering the field.

Useful? React with 👍 / 👎.

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.

2 participants