github.com/google/osv-scalibr@v0.4.1/detector/weakcredentials/codeserver/codeserver.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package codeserver contains a detector for weak credentials in Code-Server https://github.com/coder/code-server/. 16 package codeserver 17 18 import ( 19 "bufio" 20 "context" 21 "net" 22 "net/http" 23 "net/http/cookiejar" 24 "runtime" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/google/osv-scalibr/detector" 30 scalibrfs "github.com/google/osv-scalibr/fs" 31 "github.com/google/osv-scalibr/inventory" 32 "github.com/google/osv-scalibr/packageindex" 33 "github.com/google/osv-scalibr/plugin" 34 ) 35 36 /* 37 ** To test this detector, you can use the following docker image: 38 ** 39 ** docker run -it --name code-server-noauth -p 127.0.0.1:8080:8080 \ 40 ** -v "/root/code-server-configs/config-without-auth.yaml:/root/.config/code-server/config.yaml" \ 41 ** -v "$PWD:/home/coder/project" \ 42 ** -u "$(id -u):$(id -g)" \ 43 ** -e "DOCKER_USER=$USER" \ 44 ** codercom/code-server:latest 45 ** 46 ** with config-without-auth.yaml being: 47 ** bind-addr: 127.0.0.1:8080 48 ** auth: none 49 ** password: doesntmatter 50 ** cert: false 51 */ 52 53 const ( 54 // Name of the detector. 55 Name = "weakcredentials/codeserver" 56 57 // The number of requests that this detector sends. Used to compute an upper-bound for certain 58 // timeouts. 59 numRequests = 1 60 61 // defaultClientTimeout is the default timeout for the HTTP client. This means that this timeout 62 // get applied to *every request*. So, to get the timeout of the detector it has to be multiplied 63 // by the number of HTTP requests. 64 defaultClientTimeout = 1 * time.Second 65 66 // This target will specifically target a local instance of Code-Server. Note that we use 67 // 127.0.0.2 to exclude instances only listening on localhost. 68 defaultAddress = "127.0.0.2" 69 // The default address for MacOS, as only 127.0.0.1 is enabled by default. 70 defaultMacOSAddress = "localhost" 71 defaultPort = 49363 72 ) 73 74 // Patterns to differentiate enabled authentication from disabled. 75 // Tested on Code-Server v4.99.0. 76 var ( 77 authDisabledPattern1 = `<meta id="vscode-workbench-auth-session" data-settings="">` 78 authDisabledPattern2 = `globalThis._VSCODE_FILE_ROOT` 79 ) 80 81 // Config for this detector. 82 type Config struct { 83 Remote string 84 ClientTimeout time.Duration 85 } 86 87 // Detector is a SCALIBR Detector for weak/guessable passwords for the Code-Server service. 88 type Detector struct { 89 config Config 90 } 91 92 // DefaultConfig returns the default config for this detector. 93 func DefaultConfig() Config { 94 return defaultConfigWithOS(runtime.GOOS) 95 } 96 97 func defaultConfigWithOS(os string) Config { 98 address := defaultAddress 99 if os == "darwin" { 100 address = defaultMacOSAddress 101 } 102 return Config{ 103 Remote: "http://" + net.JoinHostPort(address, strconv.Itoa(defaultPort)), 104 ClientTimeout: defaultClientTimeout, 105 } 106 } 107 108 // New returns a detector. 109 func New(cfg Config) detector.Detector { 110 return &Detector{ 111 config: cfg, 112 } 113 } 114 115 // NewDefault returns a detector with the default config settings. 116 func NewDefault() detector.Detector { 117 return New(DefaultConfig()) 118 } 119 120 // Name of the detector. 121 func (Detector) Name() string { return Name } 122 123 // Version of the detector. 124 func (Detector) Version() int { return 0 } 125 126 // Requirements of the detector. 127 func (Detector) Requirements() *plugin.Capabilities { 128 return &plugin.Capabilities{ 129 RunningSystem: true, 130 } 131 } 132 133 // RequiredExtractors returns an empty list as there are no dependencies. 134 func (Detector) RequiredExtractors() []string { 135 return []string{} 136 } 137 138 // DetectedFinding returns generic vulnerability information about what is detected. 139 func (d Detector) DetectedFinding() inventory.Finding { 140 return d.finding() 141 } 142 143 func (Detector) finding() inventory.Finding { 144 return inventory.Finding{GenericFindings: []*inventory.GenericFinding{ 145 &inventory.GenericFinding{ 146 Adv: &inventory.GenericFindingAdvisory{ 147 ID: &inventory.AdvisoryID{ 148 Publisher: "SCALIBR", 149 Reference: "CODESERVER_WEAK_CREDENTIALS", 150 }, 151 Title: "Code-Server instance without authentication", 152 Description: "Your Code-Server instance has no authentication enabled. This means that the instance is vulnerable to remote code execution.", 153 Recommendation: "Enforce an authentication in the config.yaml file. See https://github.com/coder/code-server/blob/main/docs/FAQ.md#how-does-the-config-file-work for more details.", 154 Sev: inventory.SeverityCritical, 155 }, 156 }, 157 }} 158 } 159 160 // Scan starts the scan. 161 func (d Detector) Scan(ctx context.Context, _ *scalibrfs.ScanRoot, _ *packageindex.PackageIndex) (inventory.Finding, error) { 162 jar, err := cookiejar.New(nil) 163 if err != nil { 164 return inventory.Finding{}, err 165 } 166 167 client := &http.Client{ 168 Timeout: d.config.ClientTimeout, 169 Jar: jar, 170 } 171 timeout := d.config.ClientTimeout*numRequests + 100*time.Millisecond 172 ctx, cancel := context.WithTimeout(ctx, timeout) 173 defer cancel() 174 175 vuln, err := checkAuth(ctx, client, d.config.Remote) 176 if err != nil { 177 return inventory.Finding{}, err 178 } 179 180 if !vuln { 181 return inventory.Finding{}, nil 182 } 183 184 return d.finding(), nil 185 } 186 187 func checkAuth(ctx context.Context, client *http.Client, target string) (bool, error) { 188 if ctx.Err() != nil { 189 return false, ctx.Err() 190 } 191 192 req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) 193 if err != nil { 194 return false, nil 195 } 196 resp, err := client.Do(req) 197 if err != nil { 198 // Up to and including this point, we swallow errors as we consider a failure to connect to be a 199 // non-vulnerable instance. 200 return false, nil 201 } 202 203 scanner := bufio.NewScanner(resp.Body) 204 defer resp.Body.Close() 205 206 matched1 := false 207 for scanner.Scan() { 208 if ctx.Err() != nil { 209 return false, ctx.Err() 210 } 211 212 line := scanner.Text() 213 214 if !matched1 { 215 if strings.Contains(line, authDisabledPattern1) { 216 matched1 = true 217 } 218 } else { 219 if strings.Contains(line, authDisabledPattern2) { 220 return true, nil 221 } 222 } 223 } 224 225 return false, nil 226 }