Module: MCPClient::Auth::PeerText

Included in:
BrowserOAuth, OAuthProvider
Defined in:
lib/mcp_client/auth/peer_text.rb

Overview

Every string an authorization server, a resource server or a browser callback supplies passes through here before it reaches a log line or an exception message.

Those messages are not private: BrowserOAuth renders the message of a failed flow on the page it serves to the browser, and logs are read, forwarded and pasted. Peer-supplied bytes quoted verbatim carry CR/LF into a log record (splitting it into attacker-chosen lines), terminal escape sequences into a console, and unbounded response bodies into both. And the message of a JSON::ParserError quotes the token it choked on — "expected object key, got 'SECRET' at line 1 column 2" — which puts a fragment of the very body that failed to parse into the text.

So: printable, bounded text for peer strings, and position without content for a parse failure. The OAuth classes define these themselves rather than reaching for JsonRpcCommon, which they do not include: a rescue path that calls a helper its object does not have raises NoMethodError out of the very request the rescue existed to keep working.

Every helper here is TOTAL: no input can raise out of it. Nothing about a peer's bytes guarantees they are valid UTF-8 — a response body arrives as whatever the socket carried, a callback parameter as whatever CGI.unescape made of %FF, and a JSON::ParserError message quotes the undecodable bytes it choked on — and String#gsub, String#strip and Regexp#match all raise ArgumentError: invalid byte sequence in UTF-8 on them. A sanitizer that raises on the input it exists to sanitize is worse than none: it turns a peer's 400 body, or an error_description=%FF, into an exception out of the rescue path that was meant to report it. Undecodable bytes are therefore replaced before anything else looks at them.

Constant Summary collapse

PEER_TEXT_LIMIT =

How much peer-supplied text a message may carry.

200
UNDECODABLE_BYTE =

What an undecodable byte becomes. ASCII, so the result is safe to write to a log device of any encoding.

'?'
UNREADABLE_TEXT =

What a helper reports when even the replacement could not be made (an object whose #to_s raises, a string no encoding handler accepts). A helper here never raises, so there is always something to say.

'(unreadable)'

Class Method Summary collapse

Class Method Details

.decodable(text) ⇒ String

Peer bytes as valid UTF-8, and nothing else changed: no control characters removed, no truncation. This is the form peer text has to be in BEFORE it is parsed rather than printed — a WWW-Authenticate header matched for its Bearer segment, an OAuth error description matched for a redirect_uri mismatch, a callback parameter that is about to be compared, logged or rendered. String#gsub, String#match, Regexp#match?, String#split and String#strip all raise ArgumentError on bytes that are not valid UTF-8, so a peer that sends %FF turns the code that reads its error into the error. Making the bytes decodable is not enough on its own — the printable, bounded form is still what reaches a message — but it is what has to happen first, and it must happen at the point the bytes stop being bytes and start being text.

A module function, not only a private instance method, because the transports read the same WWW-Authenticate header and do not (and must not) take on the rest of this module: JsonRpcCommon already defines helpers of the same names.

Parameters:

  • text (Object) —

    peer-supplied bytes

Returns:

  • (String) —

    the same text as valid UTF-8



70
71
72
73
74
75
76
77
# File 'lib/mcp_client/auth/peer_text.rb', line 70

def self.decodable(text)
  return UNREADABLE_TEXT unless text.is_a?(String)

  utf8 = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding(Encoding::UTF_8)
  utf8.valid_encoding? ? utf8 : utf8.scrub(UNDECODABLE_BYTE)
rescue StandardError
  UNREADABLE_TEXT
end

.decodable?(text) ⇒ Boolean

Whether a peer's bytes are already text: a String that reads as valid UTF-8 as it stands. What decodable returns for anything else is a rewriting of what the peer sent — fine for a message, wrong for a value that is about to be used as a URL.

Parameters:

  • text (Object) —

    peer-supplied bytes

Returns:

  • (Boolean)


85
86
87
88
89
90
91
# File 'lib/mcp_client/auth/peer_text.rb', line 85

def self.decodable?(text)
  return false unless text.is_a?(String)

  (text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding(Encoding::UTF_8)).valid_encoding?
rescue StandardError
  false
end