44 "crypto/tls"
55 "encoding/json"
66 "fmt"
7+ "io"
78 "net"
89 "net/http"
910 "net/url"
@@ -106,11 +107,11 @@ func maskedConnection(c *models.Connection) resolvedConnection {
106107 return resolvedConnection {
107108 Type : c .Type ,
108109 Namespace : c .Namespace ,
109- URL : c .URL ,
110+ URL : redactConnectionURL ( c .URL ) ,
110111 Username : c .Username ,
111112 Password : maskValue (c .Password ),
112113 Certificate : maskValue (c .Certificate ),
113- Properties : c .Properties ,
114+ Properties : redactConnectionProperties ( c .Properties ) ,
114115 }
115116}
116117
@@ -120,23 +121,24 @@ func testConnection(ctx dbcontext.Context, c *models.Connection) testResult {
120121 if c .URL == "" {
121122 return testResult {OK : false , Message : "connection has no URL to test" }
122123 }
124+ displayURL := redactConnectionURL (c .URL )
123125 host , scheme , ok := dialTarget (c .URL , c .Type )
124126 if ! ok {
125- return testResult {OK : false , Message : fmt .Sprintf ("cannot determine host from url %q" , c . URL ), URL : c . URL }
127+ return testResult {OK : false , Message : fmt .Sprintf ("cannot determine host from url %q" , displayURL ), URL : displayURL }
126128 }
127129
128130 dialCtx , cancel := ctx .WithTimeout (5 * time .Second )
129131 defer cancel ()
130132 conn , err := (& net.Dialer {}).DialContext (dialCtx , "tcp" , host )
131133 if err != nil {
132- return testResult {OK : false , Message : fmt .Sprintf ("TCP connect to %s failed: %v" , host , err ), URL : c . URL }
134+ return testResult {OK : false , Message : fmt .Sprintf ("TCP connect to %s failed: %v" , host , err ), URL : displayURL }
133135 }
134136 _ = conn .Close ()
135137
136138 if scheme == "http" || scheme == "https" {
137- return httpProbe (c )
139+ return httpProbe (c , displayURL )
138140 }
139- return testResult {OK : true , Message : fmt .Sprintf ("TCP connect to %s succeeded" , host ), URL : c . URL }
141+ return testResult {OK : true , Message : fmt .Sprintf ("TCP connect to %s succeeded" , host ), URL : displayURL }
140142}
141143
142144// dialTarget resolves a connection URL to a host:port for the TCP reachability
@@ -208,23 +210,118 @@ func splitServerValue(v string) (host, port string) {
208210 return strings .TrimSpace (host ), port
209211}
210212
211- func httpProbe (c * models.Connection ) testResult {
213+ func httpProbe (c * models.Connection , displayURL string ) testResult {
214+ u , err := validatedHTTPProbeURL (c .URL )
215+ if err != nil {
216+ return testResult {OK : false , Message : err .Error (), URL : displayURL }
217+ }
212218 client := & http.Client {
213219 Timeout : 8 * time .Second ,
214220 Transport : & http.Transport {
215221 TLSClientConfig : & tls.Config {InsecureSkipVerify : c .InsecureTLS }, //nolint:gosec // operator opt-in via insecure_tls
216222 },
217223 }
218- resp , err := client . Get ( c . URL )
224+ req , err := http . NewRequest ( http . MethodGet , u . String (), http . NoBody )
219225 if err != nil {
220- return testResult {OK : false , Message : fmt .Sprintf ("HTTP request failed: %v" , err ), URL : c .URL }
226+ return testResult {OK : false , Message : fmt .Sprintf ("HTTP request failed: %v" , redactError (err , c .URL , displayURL )), URL : displayURL }
227+ }
228+ // codeql[go/request-forgery]: connection testing intentionally probes the
229+ // operator-supplied URL after validating it is an absolute HTTP(S) URL.
230+ resp , err := client .Do (req )
231+ if err != nil {
232+ return testResult {OK : false , Message : fmt .Sprintf ("HTTP request failed: %v" , redactError (err , c .URL , displayURL )), URL : displayURL }
221233 }
222234 defer func () { _ = resp .Body .Close () }()
235+ _ , _ = io .Copy (io .Discard , resp .Body )
223236 return testResult {
224237 OK : resp .StatusCode < 500 ,
225238 Message : fmt .Sprintf ("HTTP %s" , resp .Status ),
226- URL : c .URL ,
239+ URL : displayURL ,
240+ }
241+ }
242+
243+ func validatedHTTPProbeURL (rawURL string ) (* url.URL , error ) {
244+ u , err := url .Parse (rawURL )
245+ if err != nil {
246+ return nil , fmt .Errorf ("invalid HTTP URL: %w" , err )
247+ }
248+ if u .Scheme != "http" && u .Scheme != "https" {
249+ return nil , fmt .Errorf ("HTTP probe requires http or https URL" )
250+ }
251+ if u .Hostname () == "" {
252+ return nil , fmt .Errorf ("HTTP probe requires a URL host" )
253+ }
254+ return u , nil
255+ }
256+
257+ func redactConnectionURL (rawURL string ) string {
258+ if rawURL == "" {
259+ return ""
260+ }
261+ if u , err := url .Parse (rawURL ); err == nil && u .Scheme != "" && u .Host != "" {
262+ redacted := * u
263+ redacted .User = nil
264+ q := redacted .Query ()
265+ for key , vals := range q {
266+ if isSensitiveCredentialKey (key ) {
267+ for i := range vals {
268+ vals [i ] = "redacted"
269+ }
270+ q [key ] = vals
271+ }
272+ }
273+ redacted .RawQuery = q .Encode ()
274+ return redacted .String ()
275+ }
276+ return redactKeyValueDSN (rawURL )
277+ }
278+
279+ func redactKeyValueDSN (dsn string ) string {
280+ parts := strings .Split (dsn , ";" )
281+ for i , part := range parts {
282+ key , val , found := strings .Cut (part , "=" )
283+ if ! found || ! isSensitiveCredentialKey (key ) {
284+ continue
285+ }
286+ parts [i ] = key + "=" + maskValue (val )
287+ }
288+ return strings .Join (parts , ";" )
289+ }
290+
291+ func redactConnectionProperties (properties map [string ]string ) map [string ]string {
292+ if len (properties ) == 0 {
293+ return properties
294+ }
295+ out := make (map [string ]string , len (properties ))
296+ for key , value := range properties {
297+ if isSensitiveCredentialKey (key ) {
298+ out [key ] = maskValue (value )
299+ } else {
300+ out [key ] = value
301+ }
302+ }
303+ return out
304+ }
305+
306+ func isSensitiveCredentialKey (key string ) bool {
307+ key = strings .ToLower (strings .TrimSpace (key ))
308+ key = strings .ReplaceAll (key , "-" , "_" )
309+ key = strings .ReplaceAll (key , " " , "_" )
310+ switch key {
311+ case "password" , "pwd" , "pass" , "passwd" , "secret" , "token" , "bearer" ,
312+ "access_token" , "refresh_token" , "api_key" , "apikey" , "client_secret" ,
313+ "user" , "username" , "user_id" , "userid" , "uid" :
314+ return true
315+ default :
316+ return false
317+ }
318+ }
319+
320+ func redactError (err error , rawURL , displayURL string ) error {
321+ if err == nil {
322+ return nil
227323 }
324+ return fmt .Errorf ("%s" , strings .ReplaceAll (err .Error (), rawURL , displayURL ))
228325}
229326
230327// defaultPort maps a URL scheme to its conventional port for the reachability
0 commit comments