@@ -877,6 +877,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop,
877877 self ._peer_addr = ''
878878 self ._peer_port = 0
879879 self ._tcp_keepalive = options .tcp_keepalive
880+ self ._utf8_decode_errors = options .utf8_decode_errors
880881 self ._owner : Optional [Union [SSHClient , SSHServer ]] = None
881882 self ._extra : Dict [str , object ] = {}
882883
@@ -1042,6 +1043,11 @@ def logger(self) -> SSHLogger:
10421043
10431044 return self ._logger
10441045
1046+ def _decode_utf8 (self , msg_bytes ) -> str :
1047+ """Decode UTF-8 bytes, honoring utf8_decode_errors setting"""
1048+
1049+ return msg_bytes .decode ('utf-8' , self ._utf8_decode_errors )
1050+
10451051 def _cleanup (self , exc : Optional [Exception ]) -> None :
10461052 """Clean up this connection"""
10471053
@@ -2193,7 +2199,7 @@ def _process_disconnect(self, _pkttype: int, _pktid: int,
21932199 packet .check_end ()
21942200
21952201 try :
2196- reason = reason_bytes . decode ( 'utf-8' )
2202+ reason = self . _decode_utf8 ( reason_bytes )
21972203 lang = lang_bytes .decode ('ascii' )
21982204 except UnicodeDecodeError :
21992205 raise ProtocolError ('Invalid disconnect message' ) from None
@@ -2236,7 +2242,7 @@ def _process_debug(self, _pkttype: int, _pktid: int,
22362242 packet .check_end ()
22372243
22382244 try :
2239- msg = msg_bytes . decode ( 'utf-8' )
2245+ msg = self . _decode_utf8 ( msg_bytes )
22402246 lang = lang_bytes .decode ('ascii' )
22412247 except UnicodeDecodeError :
22422248 raise ProtocolError ('Invalid debug message' ) from None
@@ -2638,7 +2644,7 @@ def _process_userauth_banner(self, _pkttype: int, _pktid: int,
26382644 packet .check_end ()
26392645
26402646 try :
2641- msg = msg_bytes . decode ( 'utf-8' )
2647+ msg = self . _decode_utf8 ( msg_bytes )
26422648 lang = lang_bytes .decode ('ascii' )
26432649 except UnicodeDecodeError :
26442650 raise ProtocolError ('Invalid userauth banner' ) from None
@@ -2755,7 +2761,7 @@ def _process_channel_open_failure(self, _pkttype: int, _pktid: int,
27552761 packet .check_end ()
27562762
27572763 try :
2758- reason = reason_bytes . decode ( 'utf-8' )
2764+ reason = self . _decode_utf8 ( reason_bytes )
27592765 lang = lang_bytes .decode ('ascii' )
27602766 except UnicodeDecodeError :
27612767 raise ProtocolError ('Invalid channel open failure' ) from None
@@ -4373,8 +4379,8 @@ async def create_session(self, session_factory: SSHClientSessionFactory,
43734379 window : int
43744380 max_pktsize : int
43754381
4376- chan = SSHClientChannel (self , self ._loop , encoding , errors ,
4377- window , max_pktsize )
4382+ chan = SSHClientChannel (self , self ._loop , self . _utf8_decode_errors ,
4383+ encoding , errors , window , max_pktsize )
43784384
43794385 session = await chan .create (session_factory , command , subsystem ,
43804386 new_env , request_pty , term_type , term_size ,
@@ -5745,9 +5751,9 @@ async def start_sftp_client(self, env: DefTuple[Optional[Env]] = (),
57455751 env = env , send_env = send_env ,
57465752 encoding = None )
57475753
5748- return await start_sftp_client (self , self ._loop , reader , writer ,
5749- path_encoding , path_errors ,
5750- sftp_version )
5754+ return await start_sftp_client (self , self ._loop ,
5755+ self . _utf8_decode_errors , reader , writer ,
5756+ path_encoding , path_errors , sftp_version )
57515757
57525758
57535759class SSHServerConnection (SSHConnection ):
@@ -7278,6 +7284,7 @@ class SSHConnectionOptions(Options, Generic[_Options]):
72787284 family : int
72797285 local_addr : HostPort
72807286 tcp_keepalive : bool
7287+ utf8_decode_errors : str
72817288 canonicalize_hostname : Union [bool , str ]
72827289 canonical_domains : Sequence [str ]
72837290 canonicalize_fallback_local : bool
@@ -7323,6 +7330,7 @@ def prepare(self, config: SSHConfig, # type: ignore
73237330 passphrase : Optional [BytesOrStr ],
73247331 proxy_command : DefTuple [_ProxyCommand ], family : DefTuple [int ],
73257332 local_addr : DefTuple [HostPort ], tcp_keepalive : DefTuple [bool ],
7333+ utf8_decode_errors : str ,
73267334 canonicalize_hostname : DefTuple [Union [bool , str ]],
73277335 canonical_domains : DefTuple [Sequence [str ]],
73287336 canonicalize_fallback_local : DefTuple [bool ],
@@ -7387,6 +7395,8 @@ def _split_cname_patterns(
73877395 self .tcp_keepalive = cast (bool , tcp_keepalive if tcp_keepalive != ()
73887396 else config .get ('TCPKeepAlive' , True ))
73897397
7398+ self .utf8_decode_errors = utf8_decode_errors
7399+
73907400 self .canonicalize_hostname = \
73917401 cast (Union [bool , str ], canonicalize_hostname
73927402 if canonicalize_hostname != ()
@@ -7812,6 +7822,13 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
78127822 :param tcp_keepalive: (optional)
78137823 Whether or not to enable keepalive probes at the TCP level to
78147824 detect broken connections, defaulting to `True`.
7825+ :param utf8_decode_errors: (optional)
7826+ Error handling strategy to apply when UTF-8 decode errors
7827+ occur in SSH protocol messages, defaulting to 'strict'
7828+ which shuts down the connection with a ProtocolError.
7829+ Choosing other strategies can allow the message parsing
7830+ to proceed with invalid bytes in the message being removed
7831+ or replaced.
78157832 :param canonicalize_hostname: (optional)
78167833 Whether or not to enable hostname canonicalization, defaulting
78177834 to `False`, in which case hostnames are passed as-is to the
@@ -7984,6 +8001,7 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
79848001 :type keepalive_interval: *see* :ref:`SpecifyingTimeIntervals`
79858002 :type keepalive_count_max: `int`
79868003 :type tcp_keepalive: `bool`
8004+ :type utf8_decode_errors: `str`
79878005 :type canonicalize_hostname: `bool` or `'always'`
79888006 :type canonical_domains: `list` of `str`
79898007 :type canonicalize_fallback_local: `bool`
@@ -8069,6 +8087,7 @@ def prepare(self, # type: ignore
80698087 family : DefTuple [int ] = (),
80708088 local_addr : DefTuple [HostPort ] = (),
80718089 tcp_keepalive : DefTuple [bool ] = (),
8090+ utf8_decode_errors : str = 'strict' ,
80728091 canonicalize_hostname : DefTuple [Union [bool , str ]] = (),
80738092 canonical_domains : DefTuple [Sequence [str ]] = (),
80748093 canonicalize_fallback_local : DefTuple [bool ] = (),
@@ -8180,10 +8199,11 @@ def prepare(self, # type: ignore
81808199
81818200 super ().prepare (config , client_factory or SSHClient , client_version ,
81828201 host , port , tunnel , passphrase , proxy_command , family ,
8183- local_addr , tcp_keepalive , canonicalize_hostname ,
8184- canonical_domains , canonicalize_fallback_local ,
8185- canonicalize_max_dots , canonicalize_permitted_cnames ,
8186- kex_algs , encryption_algs , mac_algs , compression_algs ,
8202+ local_addr , tcp_keepalive , utf8_decode_errors ,
8203+ canonicalize_hostname , canonical_domains ,
8204+ canonicalize_fallback_local , canonicalize_max_dots ,
8205+ canonicalize_permitted_cnames , kex_algs ,
8206+ encryption_algs , mac_algs , compression_algs ,
81878207 signature_algs , host_based_auth , public_key_auth ,
81888208 kbdint_auth , password_auth , x509_trusted_certs ,
81898209 x509_trusted_cert_paths , x509_purposes , rekey_bytes ,
@@ -8636,6 +8656,13 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
86368656 :param tcp_keepalive: (optional)
86378657 Whether or not to enable keepalive probes at the TCP level to
86388658 detect broken connections, defaulting to `True`.
8659+ :param utf8_decode_errors: (optional)
8660+ Error handling strategy to apply when UTF-8 decode errors
8661+ occur in SSH protocol messages, defaulting to 'strict'
8662+ which shuts down the connection with a ProtocolError.
8663+ Choosing other strategies can allow the message parsing
8664+ to proceed with invalid bytes in the message being removed
8665+ or replaced.
86398666 :param canonicalize_hostname: (optional)
86408667 Whether or not to enable hostname canonicalization, defaulting
86418668 to `False`, in which case hostnames are passed as-is to the
@@ -8732,6 +8759,7 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
87328759 :type keepalive_interval: *see* :ref:`SpecifyingTimeIntervals`
87338760 :type keepalive_count_max: `int`
87348761 :type tcp_keepalive: `bool`
8762+ :type utf8_decode_errors: `str`
87358763 :type canonicalize_hostname: `bool` or `'always'`
87368764 :type canonical_domains: `list` of `str`
87378765 :type canonicalize_fallback_local: `bool`
@@ -8790,6 +8818,7 @@ def prepare(self, # type: ignore
87908818 family : DefTuple [int ] = (),
87918819 local_addr : DefTuple [HostPort ] = (),
87928820 tcp_keepalive : DefTuple [bool ] = (),
8821+ utf8_decode_errors : str = 'strict' ,
87938822 canonicalize_hostname : DefTuple [Union [bool , str ]] = (),
87948823 canonical_domains : DefTuple [Sequence [str ]] = (),
87958824 canonicalize_fallback_local : DefTuple [bool ] = (),
@@ -8865,10 +8894,11 @@ def prepare(self, # type: ignore
88658894
88668895 super ().prepare (config , server_factory or SSHServer , server_version ,
88678896 host , port , tunnel , passphrase , proxy_command , family ,
8868- local_addr , tcp_keepalive , canonicalize_hostname ,
8869- canonical_domains , canonicalize_fallback_local ,
8870- canonicalize_max_dots , canonicalize_permitted_cnames ,
8871- kex_algs , encryption_algs , mac_algs , compression_algs ,
8897+ local_addr , tcp_keepalive , utf8_decode_errors ,
8898+ canonicalize_hostname , canonical_domains ,
8899+ canonicalize_fallback_local , canonicalize_max_dots ,
8900+ canonicalize_permitted_cnames , kex_algs ,
8901+ encryption_algs , mac_algs , compression_algs ,
88728902 signature_algs , host_based_auth , public_key_auth ,
88738903 kbdint_auth , password_auth , x509_trusted_certs ,
88748904 x509_trusted_cert_paths , x509_purposes ,
0 commit comments