You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2026/10/05 23:21:55 UTC
(couchdb-gun) 05/16: Reject a SOCKS5 auth method that was never offered
This is an automated email from the ASF dual-hosted git repository.
nickva pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-gun.git
commit 56842ff251121ed0a069970788197bdd9b312854
Author: Loïc Hoguin <es...@ninenines.eu>
AuthorDate: Fri Sep 25 21:40:06 2026 +0200
Reject a SOCKS5 auth method that was never offered
---
src/gun_socks.erl | 43 ++++++++++++------
test/socks_SUITE.erl | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 151 insertions(+), 12 deletions(-)
diff --git a/src/gun_socks.erl b/src/gun_socks.erl
index b50d5bc..1e98a15 100644
--- a/src/gun_socks.erl
+++ b/src/gun_socks.erl
@@ -23,7 +23,7 @@
-export([handle/5]).
-export([closing/4]).
-export([close/4]).
-%% @todo down
+-export([down/1]).
-record(socks_state, {
ref :: undefined | gun:stream_ref(),
@@ -108,21 +108,34 @@ handle(Data, State, CookieStore, _, EvHandlerState) ->
{handle(Data, State), CookieStore, EvHandlerState}.
%% No authentication.
-handle(<<5, 0>>, State=#socks_state{version=5, status=auth_method_select}) ->
- case send_socks5_connect(State) of
- ok -> {state, State#socks_state{status=connect}};
- Error={error, _} -> Error
+handle(<<5, 0>>, State=#socks_state{opts=Opts, version=5, status=auth_method_select}) ->
+ case lists:member(none, maps:get(auth, Opts, [none])) of
+ true ->
+ case send_socks5_connect(State) of
+ ok -> {state, State#socks_state{status=connect}};
+ Error={error, _} -> Error
+ end;
+ false ->
+ {error, {socks5, {auth_method_not_offered, none}}}
end;
%% Username/password authentication.
-handle(<<5, 2>>, State=#socks_state{socket=Socket, transport=Transport, opts=#{auth := AuthMethods},
+handle(<<5, 2>>, State=#socks_state{socket=Socket, transport=Transport, opts=Opts,
version=5, status=auth_method_select}) ->
- [{username_password, Username, Password}] = [Method || Method <- AuthMethods],
- ULen = byte_size(Username),
- PLen = byte_size(Password),
- case Transport:send(Socket, <<1, ULen, Username/binary, PLen, Password/binary>>) of
- ok -> {state, State#socks_state{status=auth_username_password}};
- Error={error, _} -> Error
+ case lists:keyfind(username_password, 1, maps:get(auth, Opts, [none])) of
+ {username_password, Username, Password} ->
+ ULen = byte_size(Username),
+ PLen = byte_size(Password),
+ case Transport:send(Socket, <<1, ULen, Username/binary, PLen, Password/binary>>) of
+ ok -> {state, State#socks_state{status=auth_username_password}};
+ Error={error, _} -> Error
+ end;
+ false ->
+ {error, {socks5, {auth_method_not_offered, username_password}}}
end;
+handle(<<5, 255>>, #socks_state{version=5, status=auth_method_select}) ->
+ {error, {socks5, no_acceptable_auth_method}};
+handle(<<5, Method>>, #socks_state{version=5, status=auth_method_select}) ->
+ {error, {socks5, {auth_method_not_offered, Method}}};
%% Username/password authentication successful.
handle(<<1, 0>>, State=#socks_state{version=5, status=auth_username_password}) ->
case send_socks5_connect(State) of
@@ -205,3 +218,9 @@ closing(_, _, _, EvHandlerState) ->
close(_, _, _, EvHandlerState) ->
EvHandlerState.
+
+%% No tunnel stream exists until the handshake completes.
+down(#socks_state{ref=undefined}) ->
+ [];
+down(#socks_state{ref=Ref}) ->
+ [Ref].
diff --git a/test/socks_SUITE.erl b/test/socks_SUITE.erl
index 4f0a1e0..47c0787 100644
--- a/test/socks_SUITE.erl
+++ b/test/socks_SUITE.erl
@@ -145,6 +145,126 @@ do_auth_method({username_password, _, _}) -> username_password.
%% Tests.
+unoffered_auth_method_no_crash(_) ->
+ doc("A proxy selecting an auth method that was never offered "
+ "must not crash the connection."),
+ {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
+ {ok, {_, ProxyPort}} = inet:sockname(ListenSocket),
+ spawn_link(fun() ->
+ {ok, Socket} = gen_tcp:accept(ListenSocket, 5000),
+ %% Only "no auth" was offered. The proxy selects username/password.
+ {ok, <<5, 1, 0>>} = gen_tcp:recv(Socket, 0, 5000),
+ ok = gen_tcp:send(Socket, <<5, 2>>),
+ receive after infinity -> ok end
+ end),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ protocols => [{socks, #{
+ auth => [none],
+ host => "localhost",
+ port => 1234,
+ transport => tcp,
+ protocols => [http]
+ }}]
+ }),
+ {ok, socks} = gun:await_up(ConnPid),
+ Reason = receive
+ {gun_down, ConnPid, socks, Reason0, _} -> Reason0
+ after 5000 ->
+ error(timeout)
+ end,
+ {error, {socks5, {auth_method_not_offered, username_password}}} = Reason,
+ gun:close(ConnPid).
+
+auth_method_downgrade_rejected(_) ->
+ doc("A proxy selecting no authentication when only username/password "
+ "was offered must be rejected."),
+ {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
+ {ok, {_, ProxyPort}} = inet:sockname(ListenSocket),
+ spawn_link(fun() ->
+ {ok, Socket} = gen_tcp:accept(ListenSocket, 5000),
+ %% Only username/password was offered. The proxy selects no auth.
+ {ok, <<5, 1, 2>>} = gen_tcp:recv(Socket, 0, 5000),
+ ok = gen_tcp:send(Socket, <<5, 0>>),
+ receive after infinity -> ok end
+ end),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ protocols => [{socks, #{
+ auth => [{username_password, <<"user">>, <<"password">>}],
+ host => "localhost",
+ port => 1234,
+ transport => tcp,
+ protocols => [http]
+ }}]
+ }),
+ {ok, socks} = gun:await_up(ConnPid),
+ Reason = receive
+ {gun_down, ConnPid, socks, Reason0, _} -> Reason0
+ after 5000 ->
+ error(timeout)
+ end,
+ {error, {socks5, {auth_method_not_offered, none}}} = Reason,
+ gun:close(ConnPid).
+
+unknown_auth_method_no_crash(_) ->
+ doc("A proxy selecting an auth method that was never offered "
+ "must not crash the connection. (RFC1928 3)"),
+ {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
+ {ok, {_, ProxyPort}} = inet:sockname(ListenSocket),
+ spawn_link(fun() ->
+ {ok, Socket} = gen_tcp:accept(ListenSocket, 5000),
+ %% Only "no auth" was offered. The proxy selects GSSAPI.
+ {ok, <<5, 1, 0>>} = gen_tcp:recv(Socket, 0, 5000),
+ ok = gen_tcp:send(Socket, <<5, 1>>),
+ receive after infinity -> ok end
+ end),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ protocols => [{socks, #{
+ auth => [none],
+ host => "localhost",
+ port => 1234,
+ transport => tcp,
+ protocols => [http]
+ }}]
+ }),
+ {ok, socks} = gun:await_up(ConnPid),
+ Reason = receive
+ {gun_down, ConnPid, socks, Reason0, _} -> Reason0
+ after 5000 ->
+ error(timeout)
+ end,
+ {error, {socks5, {auth_method_not_offered, 1}}} = Reason,
+ gun:close(ConnPid).
+
+no_acceptable_auth_method(_) ->
+ doc("A proxy reporting that no offered auth method is acceptable "
+ "must not crash the connection. (RFC1928 3)"),
+ {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
+ {ok, {_, ProxyPort}} = inet:sockname(ListenSocket),
+ spawn_link(fun() ->
+ {ok, Socket} = gen_tcp:accept(ListenSocket, 5000),
+ {ok, <<5, 1, 0>>} = gen_tcp:recv(Socket, 0, 5000),
+ %% X'FF': none of the methods listed by the client are acceptable.
+ ok = gen_tcp:send(Socket, <<5, 255>>),
+ receive after infinity -> ok end
+ end),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ protocols => [{socks, #{
+ auth => [none],
+ host => "localhost",
+ port => 1234,
+ transport => tcp,
+ protocols => [http]
+ }}]
+ }),
+ {ok, socks} = gun:await_up(ConnPid),
+ Reason = receive
+ {gun_down, ConnPid, socks, Reason0, _} -> Reason0
+ after 5000 ->
+ error(timeout)
+ end,
+ {error, {socks5, no_acceptable_auth_method}} = Reason,
+ gun:close(ConnPid).
+
socks5_tcp_http_none(_) ->
doc("Use Socks5 over TCP and without authentication to connect to an HTTP server."),
do_socks5(<<"http">>, tcp, http, tcp, none).