Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ Unreleased
bracketed IPv6 address, or behind a trailing dot, are now refused like
other IP literals. The earlier fix for CVE-2026-47076 missed these two
spellings, so such a URL still reached the address it denotes (#987).
- HTTPS proxies reached through CONNECT now get the same certificate checks
as the target: hackney's CA bundle and a hostname check against the
proxy. The connection to the proxy only set the server name, so on
OTP 27 and later it failed unless `proxy_ssl_options` set `cacerts` or
`verify_none`. An explicit `{verify, verify_none}` is still honoured (#988).

### Changed

Expand Down
23 changes: 16 additions & 7 deletions src/hackney_http_connect.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,26 @@
ssl_opts(Host, Opts) ->
hackney_ssl:ssl_opts(Host, Opts).

%% @doc TLS options for the proxy leg. Add SNI for the proxy host unless the
%% user already set one or the proxy is addressed by IP literal (RFC 6066
%% forbids SNI for IP literals).
%% @doc TLS options for the proxy leg: the same verification as the target
%% leg (verify_peer, the bundled CA store, a hostname check against the proxy
%% host) with `proxy_ssl_options' on top. A caller who sets
%% `{verify, verify_none}' gets their options as given. SNI is set to the
%% proxy host unless the user set one or the proxy is addressed by IP literal
%% (RFC 6066 forbids SNI for IP literals).
-spec proxy_ssl_opts(string() | binary(), list()) -> list().
proxy_ssl_opts(ProxyHost, ProxySslOpts) when is_binary(ProxyHost) ->
proxy_ssl_opts(binary_to_list(ProxyHost), ProxySslOpts);
proxy_ssl_opts(ProxyHost, ProxySslOpts) ->
case lists:keymember(server_name_indication, 1, ProxySslOpts) of
true -> ProxySslOpts;
Opts = case proplists:get_value(verify, ProxySslOpts) of
verify_none -> ProxySslOpts;
_ -> hackney_ssl:ssl_opts(ProxyHost, [{ssl_options, ProxySslOpts}])
end,
case lists:keymember(server_name_indication, 1, Opts) of
true -> Opts;
false ->
case hackney_url:is_ip_literal(ProxyHost) of
true -> ProxySslOpts;
false -> [{server_name_indication, ProxyHost} | ProxySslOpts]
true -> Opts;
false -> [{server_name_indication, ProxyHost} | Opts]
end
end.

Expand Down
23 changes: 23 additions & 0 deletions test/hackney_proxy_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,29 @@ proxy_ssl_opts_ipv6_test() ->
Opts = hackney_http_connect:proxy_ssl_opts("::1", []),
?assertNot(proplists:is_defined(server_name_indication, Opts)).

%% The proxy leg verifies the proxy's certificate like the target leg does;
%% it used to carry SNI only, which OTP 27+ refuses to connect with.
proxy_ssl_opts_verifies_by_default_test() ->
Opts = hackney_http_connect:proxy_ssl_opts("proxy.example", []),
?assertEqual(verify_peer, proplists:get_value(verify, Opts)),
?assertMatch([_ | _], proplists:get_value(cacerts, Opts)),
?assert(proplists:is_defined(verify_fun, Opts)).

%% An explicit opt-out is used as given, with SNI still set.
proxy_ssl_opts_verify_none_as_given_test() ->
Opts = hackney_http_connect:proxy_ssl_opts("proxy.example",
[{verify, verify_none}]),
?assertEqual(verify_none, proplists:get_value(verify, Opts)),
?assertNot(proplists:is_defined(verify_fun, Opts)),
?assertEqual("proxy.example", proplists:get_value(server_name_indication, Opts)).

%% Caller trust options are merged over the defaults, not replacing them.
proxy_ssl_opts_custom_ca_test() ->
Opts = hackney_http_connect:proxy_ssl_opts("proxy.example",
[{cacerts, [<<"der">>]}]),
?assertEqual(verify_peer, proplists:get_value(verify, Opts)),
?assertEqual([<<"der">>], proplists:get_value(cacerts, Opts)).

proxy_ssl_opts_user_sni_test() ->
Opts = hackney_http_connect:proxy_ssl_opts(
"proxy.example", [{server_name_indication, "set.by.user"}]),
Expand Down
Loading