Skip to content

Commit 6eef4d0

Browse files
committed
Strip Authorization header on cross-origin redirects
When following a redirect to a different origin (scheme, host, or port), the Authorization header is now removed to prevent credential leakage. Previously, all headers were forwarded unconditionally to the redirect target, which could expose auth tokens to unintended hosts (e.g., when redirecting from a custom origin to S3). Closes #770.
1 parent ddffab3 commit 6eef4d0

4 files changed

Lines changed: 112 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
### Fixed
2020

21+
- Strip `Authorization` header when following redirects to a different origin
22+
(scheme, host, or port) to prevent credential leakage (#770)
2123
- AutoInflate now preserves the response charset encoding instead of
2224
defaulting to `Encoding::BINARY` (#535)
2325
- `LocalJumpError` when using instrumentation with instrumenters that

lib/http/request.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ def redirect(uri, verb = @verb)
171171
headers = self.headers.dup
172172
headers.delete(Headers::HOST)
173173

174+
redirect_uri = @uri.join(uri)
175+
176+
# Strip sensitive auth headers when redirecting to a different origin
177+
# (scheme + host + port) to prevent credential leakage.
178+
# See: https://github.com/httprb/http/issues/770
179+
headers.delete(Headers::AUTHORIZATION) unless @uri.origin == redirect_uri.origin
180+
174181
new_body = body.source
175182
if verb == :get
176183
# request bodies should not always be resubmitted when following a redirect
@@ -187,7 +194,7 @@ def redirect(uri, verb = @verb)
187194

188195
self.class.new(
189196
verb: verb,
190-
uri: @uri.join(uri),
197+
uri: redirect_uri,
191198
headers: headers,
192199
proxy: proxy,
193200
body: new_body,

test/http/redirector_test.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,64 @@ def empty_body
650650
end
651651
end
652652

653+
context "with Authorization header" do
654+
it "preserves Authorization when redirecting to same origin" do
655+
req = HTTP::Request.new verb: :get, uri: "http://example.com"
656+
req.headers.set("Authorization", "Bearer secret")
657+
hops = [
658+
redirect_response(301, "http://example.com/other"),
659+
simple_response(200, "done")
660+
]
661+
662+
redirector.perform(req, hops.shift) do |request|
663+
assert_equal "Bearer secret", request["Authorization"]
664+
hops.shift
665+
end
666+
end
667+
668+
it "strips Authorization when redirecting to different host" do
669+
req = HTTP::Request.new verb: :get, uri: "http://example.com"
670+
req.headers.set("Authorization", "Bearer secret")
671+
hops = [
672+
redirect_response(301, "http://other.example.com/"),
673+
simple_response(200, "done")
674+
]
675+
676+
redirector.perform(req, hops.shift) do |request|
677+
assert_nil request["Authorization"]
678+
hops.shift
679+
end
680+
end
681+
682+
it "strips Authorization when redirecting to different scheme" do
683+
req = HTTP::Request.new verb: :get, uri: "http://example.com"
684+
req.headers.set("Authorization", "Bearer secret")
685+
hops = [
686+
redirect_response(301, "https://example.com/"),
687+
simple_response(200, "done")
688+
]
689+
690+
redirector.perform(req, hops.shift) do |request|
691+
assert_nil request["Authorization"]
692+
hops.shift
693+
end
694+
end
695+
696+
it "strips Authorization when redirecting to different port" do
697+
req = HTTP::Request.new verb: :get, uri: "http://example.com"
698+
req.headers.set("Authorization", "Bearer secret")
699+
hops = [
700+
redirect_response(301, "http://example.com:8080/"),
701+
simple_response(200, "done")
702+
]
703+
704+
redirector.perform(req, hops.shift) do |request|
705+
assert_nil request["Authorization"]
706+
hops.shift
707+
end
708+
end
709+
end
710+
653711
it "does not falsely detect endless loop when verb changes for same URL" do
654712
req = HTTP::Request.new verb: :post, uri: "http://example.com"
655713
# POST http://example.com → 302 → GET http://example.com → 302 → GET http://example.com/done → 200

test/http/request_test.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,50 @@
258258
assert_equal :get, redirected_with_verb.verb
259259
end
260260
end
261+
262+
context "with Authorization header" do
263+
let(:headers) { { accept: "text/html", authorization: "Bearer token123" } }
264+
265+
context "when redirecting to same origin" do
266+
let(:redirected) { request.redirect "/other-path" }
267+
268+
it "preserves Authorization header" do
269+
assert_equal "Bearer token123", redirected["Authorization"]
270+
end
271+
end
272+
273+
context "when redirecting to different host" do
274+
let(:redirected) { request.redirect "http://other.example.com/" }
275+
276+
it "strips Authorization header" do
277+
assert_nil redirected["Authorization"]
278+
end
279+
end
280+
281+
context "when redirecting to different scheme" do
282+
let(:redirected) { request.redirect "https://example.com/" }
283+
284+
it "strips Authorization header" do
285+
assert_nil redirected["Authorization"]
286+
end
287+
end
288+
289+
context "when redirecting to different port" do
290+
let(:redirected) { request.redirect "http://example.com:8080/" }
291+
292+
it "strips Authorization header" do
293+
assert_nil redirected["Authorization"]
294+
end
295+
end
296+
297+
context "when redirecting to schema-less URL with different host" do
298+
let(:redirected) { request.redirect "//other.example.com/path" }
299+
300+
it "strips Authorization header" do
301+
assert_nil redirected["Authorization"]
302+
end
303+
end
304+
end
261305
end
262306

263307
describe "#headline" do

0 commit comments

Comments
 (0)