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
-
.decodable(text) ⇒ String
Peer bytes as valid UTF-8, and nothing else changed: no control characters removed, no truncation.
-
.decodable?(text) ⇒ Boolean
Whether a peer's bytes are already text: a String that reads as valid UTF-8 as it stands.
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.
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.
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 |