@@ -162,3 +162,82 @@ func TestBearerAuthTransport_DoesNotMutateOriginalRequest(t *testing.T) {
162162
163163 assert .Empty (t , req .Header .Get (headers .AuthorizationHeader ), "original request must not be mutated" )
164164}
165+
166+ // hostRecordingTransport records the Authorization header seen for each request
167+ // host, so a test can assert what the token would be attached to without a live
168+ // network. It stands in for the real transport at the bottom of the chain.
169+ type hostRecordingTransport struct {
170+ authByHost map [string ]string
171+ }
172+
173+ func (h * hostRecordingTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
174+ h .authByHost [req .URL .Hostname ()] = req .Header .Get (headers .AuthorizationHeader )
175+ return & http.Response {
176+ StatusCode : http .StatusOK ,
177+ Body : http .NoBody ,
178+ Header : make (http.Header ),
179+ Request : req ,
180+ }, nil
181+ }
182+
183+ // TestBearerAuthTransport_HostScoping verifies that when AllowedHosts is set,
184+ // the token is attached to a request on an allowed host but withheld from a
185+ // request to any other host. A redirect off the configured GitHub hosts arrives
186+ // here as a RoundTrip to a different host, so this is the property that keeps
187+ // the token from following such a redirect. net/http's own cross-host stripping
188+ // does not cover it, because this transport re-adds the header on every hop.
189+ //
190+ // The hosts are distinct hostnames (matching the real case: api.github.com
191+ // versus objects.githubusercontent.com) rather than two loopback servers on
192+ // different ports, because AllowedHosts matches on hostname and ignores port.
193+ func TestBearerAuthTransport_HostScoping (t * testing.T ) {
194+ t .Parallel ()
195+
196+ rec := & hostRecordingTransport {authByHost : map [string ]string {}}
197+ rt := & BearerAuthTransport {
198+ Transport : rec ,
199+ Token : "secret-token" ,
200+ AllowedHosts : []string {"api.github.com" , "raw.githubusercontent.com" },
201+ }
202+
203+ for _ , target := range []string {
204+ "https://api.github.com/repos/o/r" ,
205+ "https://raw.githubusercontent.com/o/r/main/f" , // allowed, different host
206+ "https://objects.githubusercontent.com/evil" , // redirect target, not allowed
207+ "https://attacker.example.com/steal" , // arbitrary host, not allowed
208+ } {
209+ req , err := http .NewRequestWithContext (context .Background (), http .MethodGet , target , nil )
210+ require .NoError (t , err )
211+ resp , err := rt .RoundTrip (req )
212+ require .NoError (t , err )
213+ resp .Body .Close ()
214+ }
215+
216+ assert .Equal (t , "Bearer secret-token" , rec .authByHost ["api.github.com" ],
217+ "token must be sent to an allowed host" )
218+ assert .Equal (t , "Bearer secret-token" , rec .authByHost ["raw.githubusercontent.com" ],
219+ "token must be sent to every allowed host" )
220+ assert .Empty (t , rec .authByHost ["objects.githubusercontent.com" ],
221+ "token must not be sent to a non-allowed host (a redirect target)" )
222+ assert .Empty (t , rec .authByHost ["attacker.example.com" ],
223+ "token must not be sent to an arbitrary non-allowed host" )
224+ }
225+
226+ // TestBearerAuthTransport_EmptyAllowedHostsPreservesBehavior verifies the
227+ // backward-compatible default: with no AllowedHosts, the token is attached to
228+ // every host, exactly as before this change.
229+ func TestBearerAuthTransport_EmptyAllowedHostsPreservesBehavior (t * testing.T ) {
230+ t .Parallel ()
231+
232+ rec := & hostRecordingTransport {authByHost : map [string ]string {}}
233+ rt := & BearerAuthTransport {Transport : rec , Token : "secret-token" }
234+
235+ req , err := http .NewRequestWithContext (context .Background (), http .MethodGet , "https://anywhere.example.com/x" , nil )
236+ require .NoError (t , err )
237+ resp , err := rt .RoundTrip (req )
238+ require .NoError (t , err )
239+ resp .Body .Close ()
240+
241+ assert .Equal (t , "Bearer secret-token" , rec .authByHost ["anywhere.example.com" ],
242+ "with no AllowedHosts, token attaches to every host as before" )
243+ }
0 commit comments