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).