@@ -12,6 +12,7 @@ import (
1212 "io"
1313 "log/slog"
1414 "net/http"
15+ "net/url"
1516 "os"
1617 "strings"
1718 "time"
@@ -34,10 +35,12 @@ var (
3435
3536// SetMetadataURL sets a custom metadata server URL for testing.
3637// Returns a function that restores the original URL.
37- func SetMetadataURL (url string ) func () {
38+ // WARNING: This function should only be called in test code.
39+ // Set DS9_ALLOW_TEST_OVERRIDES=true to enable in non-test environments.
40+ func SetMetadataURL (urlStr string ) func () {
3841 old := metadataURL
3942 oldTestMode := isTestMode
40- metadataURL = url
43+ metadataURL = urlStr
4144 isTestMode = true // Enable test mode to skip ADC
4245 return func () {
4346 metadataURL = old
@@ -107,10 +110,14 @@ func accessTokenFromADC(ctx context.Context) (string, error) {
107110func exchangeRefreshToken (ctx context.Context , clientID , clientSecret , refreshToken string ) (string , error ) {
108111 tokenURL := "https://oauth2.googleapis.com/token" //nolint:gosec // This is Google's OAuth2 token endpoint, not a hardcoded credential
109112
110- reqBody := fmt .Sprintf (
111- "client_id=%s&client_secret=%s&refresh_token=%s&grant_type=refresh_token" ,
112- clientID , clientSecret , refreshToken ,
113- )
113+ // Use url.Values for proper URL encoding to prevent parameter injection
114+ form := url.Values {
115+ "client_id" : {clientID },
116+ "client_secret" : {clientSecret },
117+ "refresh_token" : {refreshToken },
118+ "grant_type" : {"refresh_token" },
119+ }
120+ reqBody := form .Encode ()
114121
115122 req , err := http .NewRequestWithContext (ctx , http .MethodPost , tokenURL , strings .NewReader (reqBody ))
116123 if err != nil {
@@ -133,7 +140,9 @@ func exchangeRefreshToken(ctx context.Context, clientID, clientSecret, refreshTo
133140 if readErr != nil {
134141 return "" , fmt .Errorf ("token exchange returned %d" , resp .StatusCode )
135142 }
136- return "" , fmt .Errorf ("token exchange returned %d: %s" , resp .StatusCode , string (body ))
143+ // Log full error details but return sanitized message to prevent information leakage
144+ slog .ErrorContext (ctx , "OAuth token exchange failed" , "status" , resp .StatusCode , "response" , string (body ))
145+ return "" , fmt .Errorf ("token exchange returned %d" , resp .StatusCode )
137146 }
138147
139148 body , err := io .ReadAll (io .LimitReader (resp .Body , maxBodySize ))
@@ -156,9 +165,9 @@ func exchangeRefreshToken(ctx context.Context, clientID, clientSecret, refreshTo
156165// accessTokenFromMetadata retrieves an access token from the GCP metadata server.
157166// This is used when running on GCP (GCE, GKE, Cloud Run, etc.).
158167func accessTokenFromMetadata (ctx context.Context ) (string , error ) {
159- url := metadataURL + "/instance/service-accounts/default/token"
168+ reqURL := metadataURL + "/instance/service-accounts/default/token"
160169
161- req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , http .NoBody )
170+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , reqURL , http .NoBody )
162171 if err != nil {
163172 return "" , err
164173 }
@@ -197,9 +206,9 @@ func accessTokenFromMetadata(ctx context.Context) (string, error) {
197206
198207// ProjectID retrieves the project ID from the GCP metadata server.
199208func ProjectID (ctx context.Context ) (string , error ) {
200- url := metadataURL + "/project/project-id"
209+ reqURL := metadataURL + "/project/project-id"
201210
202- req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , http .NoBody )
211+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , reqURL , http .NoBody )
203212 if err != nil {
204213 return "" , err
205214 }
0 commit comments