Low severityNVD Advisory· Published May 28, 2025· Updated Apr 15, 2026
CVE-2025-3864
CVE-2025-3864
Description
Hackney fails to properly release HTTP connections to the pool after handling 307 Temporary Redirect responses. Remote attackers can exploit this to exhaust connection pools, causing denial of service in applications using the library. Fix for this issue has been included in 1.24.0 release.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
hackneyHex | < 1.24.0 | 1.24.0 |
Patches
18f13ddac50d1fix pool connections not freed on 307 redirects
2 files changed · +109 −4
src/hackney.erl+30 −3 modified@@ -865,7 +865,7 @@ do_connect(ProxyHost, ProxyPort, {ProxyUser, ProxyPass}, Transport, Host, Port, maybe_redirect({ok, _}=Resp, _Req) -> Resp; maybe_redirect( - {ok, S, _H, #client{headers=Headers, follow_redirect=true, retries=Tries}=Client}=Resp, + {ok, S, H, #client{headers=Headers, follow_redirect=true, retries=Tries}=Client}=Resp, Req ) when Tries > 0 -> %% check if the given location is an absolute url, @@ -1101,8 +1101,35 @@ reply_response( reply_response({ok, Status, Headers, #client{request_ref=Ref}=NState}, _State) -> case NState#client.with_body of false -> - hackney_manager:update_state(NState), - {ok, Status, Headers, Ref}; + %% For redirect responses with pools, ensure body is consumed to properly clean up connections + IsRedirect = lists:member(Status, [301, 302, 303, 307, 308]), + IsPool = hackney_connect:is_pool(NState) /= false, + case IsRedirect andalso IsPool of + true -> + %% Skip the body to ensure proper connection cleanup for pool redirects + case hackney_response:skip_body(NState) of + {skip, CleanNState} -> + %% For pools, the connection cleanup should have happened in skip_body + %% Check if this breaks existing API by checking pool name + PoolName = proplists:get_value(pool, NState#client.options, default), + case PoolName of + default -> + %% For default pool, preserve existing API compatibility + hackney_manager:update_state(CleanNState), + {ok, Status, Headers, Ref}; + _ -> + %% For custom pools, use full cleanup to ensure connection release + maybe_update_req(CleanNState), + {ok, Status, Headers, Ref} + end; + Error -> + hackney_manager:handle_error(NState), + Error + end; + false -> + hackney_manager:update_state(NState), + {ok, Status, Headers, Ref} + end; true -> reply_with_body(Status, Headers, NState) end;
test/hackney_integration_tests.erl+79 −1 modified@@ -25,7 +25,8 @@ all_tests() -> fun async_head_request/0, fun async_no_content_request/0, fun test_frees_manager_ets_when_body_is_in_client/0, - fun test_frees_manager_ets_when_body_is_in_response/0]. + fun test_frees_manager_ets_when_body_is_in_response/0, + fun test_307_redirect_pool_cleanup/0]. %%all_tests() -> %% case has_unix_socket() of @@ -206,6 +207,83 @@ test_frees_manager_ets_when_body_is_in_response() -> AfterCount = ets:info(hackney_manager_refs, size), ?assertEqual(BeforeCount, AfterCount). +%% Test for issue #307: Pool connections not freed when response code is 307 +%% Tests that POST requests with 307 redirects properly clean up pool connections +test_307_redirect_pool_cleanup() -> + %% Create a small test pool to monitor connection usage + PoolName = test_pool_307_cleanup, + PoolOpts = [{pool_size, 2}, {timeout, 5000}], + ok = hackney_pool:start_pool(PoolName, PoolOpts), + + %% URL that returns a 307 redirect (httpbin supports this) + URL = <<"http://localhost:8000/redirect-to?url=http://localhost:8000/get&status_code=307">>, + RequestOpts = [{pool, PoolName}, {follow_redirect, false}], + + %% Get initial pool stats + InitialStats = hackney_pool:get_stats(PoolName), + InitialInUse = proplists:get_value(in_use_count, InitialStats), + + %% Make a GET request that should get a 307 redirect (similar to existing test) + %% With follow_redirect=false, this should return the redirect response directly + Result1 = hackney:request(get, URL, [], <<"">>, RequestOpts), + + %% Handle response - the fourth element might be a reference or client + case Result1 of + {ok, {maybe_redirect, 307, _Headers1, Client1}} -> + {skip, _} = hackney:skip_body(Client1), + ok; + {ok, Status1, _Headers1, ClientOrRef1} when Status1 >= 300, Status1 < 400 -> + %% If it's a reference, we can't call skip_body - the connection handling is different + if is_reference(ClientOrRef1) -> + ok; %% Connection handling is managed internally for pooled requests + true -> + {skip, _} = hackney:skip_body(ClientOrRef1), + ok + end; + {ok, Status1, _Headers1, Client1} when Status1 >= 200, Status1 < 400 -> + {ok, _Body} = hackney:body(Client1), + ok; + Other -> + ?debugFmt("Unexpected response: ~p~n", [Other]) + end, + + %% Make a second request to verify pool connections are available + Result2 = hackney:request(get, URL, [], <<"">>, RequestOpts), + + case Result2 of + {ok, {maybe_redirect, 307, _Headers2, Client2}} -> + {skip, _} = hackney:skip_body(Client2), + ok; + {ok, Status2, _Headers2, ClientOrRef2} when Status2 >= 300, Status2 < 400 -> + if is_reference(ClientOrRef2) -> + ok; %% Connection handling is managed internally for pooled requests + true -> + {skip, _} = hackney:skip_body(ClientOrRef2), + ok + end; + {ok, Status2, _Headers2, Client2} when Status2 >= 200, Status2 < 400 -> + {ok, _Body2} = hackney:body(Client2), + ok; + {error, checkout_timeout} -> + %% This would indicate the pool connection was not returned + ?assert(false); + Other2 -> + ?debugFmt("Unexpected second response: ~p~n", [Other2]) + end, + + %% Allow time for async cleanup to complete + timer:sleep(10), + + %% Check final pool stats - connections should be returned + FinalStats = hackney_pool:get_stats(PoolName), + FinalInUse = proplists:get_value(in_use_count, FinalStats), + + %% The key test: in_use_count should return to initial value + ?assertEqual(InitialInUse, FinalInUse), + + %% Clean up test pool + hackney_pool:stop_pool(PoolName). + %%local_socket_request() -> %% URL = <<"http+unix://httpbin.sock/get">>,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-9fm9-hp7p-53mfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-3864ghsaADVISORY
- cert.pl/en/posts/2025/05/CVE-2025-3864ghsaWEB
- github.com/benoitc/hackney/commit/8f13ddac50d1626f8b9a47a08bd599e4efe1773dnvdWEB
- github.com/benoitc/hackney/issues/717nvdWEB
- cert.pl/en/posts/2025/05/CVE-2025-3864/nvd
News mentions
0No linked articles in our index yet.