github.com/google/osv-scalibr@v0.4.1/detector/weakcredentials/filebrowser/filebrowser.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 filebrowser implements a detector for weak/guessable passwords 16 // on a filebrowser instance. 17 // To test and install filebrowser, simply follow the instructions in 18 // https://filebrowser.org/installation 19 package filebrowser 20 21 import ( 22 "bytes" 23 "context" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "io" 28 "net" 29 "net/http" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/google/osv-scalibr/detector" 35 scalibrfs "github.com/google/osv-scalibr/fs" 36 "github.com/google/osv-scalibr/inventory" 37 "github.com/google/osv-scalibr/log" 38 "github.com/google/osv-scalibr/packageindex" 39 "github.com/google/osv-scalibr/plugin" 40 ) 41 42 const ( 43 // Name of the detector. 44 Name = "weakcredentials/filebrowser" 45 46 fileBrowserIP = "127.0.0.1" 47 requestTimeout = 2 * time.Second 48 ) 49 50 var ( 51 fileBrowserPorts = []int{ 52 5080, 53 8080, 54 80, 55 } 56 ) 57 58 // Detector is a SCALIBR Detector for weak/guessable passwords from /etc/shadow. 59 type Detector struct{} 60 61 // New returns a detector. 62 func New() detector.Detector { 63 return &Detector{} 64 } 65 66 // Name of the detector. 67 func (Detector) Name() string { return Name } 68 69 // Version of the detector. 70 func (Detector) Version() int { return 0 } 71 72 // Requirements of the detector. 73 func (Detector) Requirements() *plugin.Capabilities { 74 return &plugin.Capabilities{OS: plugin.OSLinux, RunningSystem: true} 75 } 76 77 // RequiredExtractors returns an empty list as there are no dependencies. 78 func (Detector) RequiredExtractors() []string { return []string{} } 79 80 // DetectedFinding returns generic vulnerability information about what is detected. 81 func (d Detector) DetectedFinding() inventory.Finding { 82 return d.finding() 83 } 84 85 func (Detector) finding() inventory.Finding { 86 return inventory.Finding{GenericFindings: []*inventory.GenericFinding{{ 87 Adv: &inventory.GenericFindingAdvisory{ 88 ID: &inventory.AdvisoryID{ 89 Publisher: "SCALIBR", 90 Reference: "file-browser-weakcredentials", 91 }, 92 Title: "Filebrowser default credentials", 93 Description: "Filebrowser is a self-hosted web application to manage files and folders. " + 94 "It has been detected that the default credentials are in use, which can be exploited by an" + 95 " attacker to execute arbitrary commands on the affected system.", 96 Recommendation: "If you have devlify installed, run 'devlify update' to apply the fix." + 97 " Follow the prompts until you get a 'Configuration is done!' message." + 98 " If the update succeeded, the output of the 'podman ps' command should no longer" + 99 " show the File Browser container." + 100 " In all other instances where filebrowser is installed as a stand-alone, the vulnerability" + 101 " can be remediated by changing the default credentials through the Web UI and restarting the service" + 102 " or by uninstalling the filebrowser service/container.", 103 Sev: inventory.SeverityCritical, 104 }, 105 }}} 106 } 107 108 // Scan starts the scan. 109 func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) { 110 for _, fileBrowserPort := range fileBrowserPorts { 111 if ctx.Err() != nil { 112 return inventory.Finding{}, ctx.Err() 113 } 114 if !isVulnerable(ctx, fileBrowserIP, fileBrowserPort) { 115 continue 116 } 117 return d.finding(), nil 118 } 119 120 return inventory.Finding{}, nil 121 } 122 123 func isVulnerable(ctx context.Context, fileBrowserIP string, fileBrowserPort int) bool { 124 if !checkAccessibility(ctx, fileBrowserIP, fileBrowserPort) { 125 return false 126 } 127 if !checkLogin(ctx, fileBrowserIP, fileBrowserPort) { 128 return false 129 } 130 return true 131 } 132 133 // checkAccessibility checks if the filebrowser instance is accessible given an IP and port. 134 func checkAccessibility(ctx context.Context, fileBrowserIP string, fileBrowserPort int) bool { 135 client := &http.Client{Timeout: requestTimeout} 136 targetURL := fmt.Sprintf("http://%s/", net.JoinHostPort(fileBrowserIP, strconv.Itoa(fileBrowserPort))) 137 138 req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) 139 if err != nil { 140 log.Infof("Error while constructing request %s to the server: %v", targetURL, err) 141 return false 142 } 143 144 resp, err := client.Do(req) 145 if err != nil { 146 if errors.Is(err, context.DeadlineExceeded) { 147 log.Infof("Timeout exceeded when accessing %s", targetURL) 148 } else { 149 log.Debugf("Error when sending request %s to the server: %v", targetURL, err) 150 } 151 return false 152 } 153 defer resp.Body.Close() 154 155 if resp.StatusCode != http.StatusOK { 156 return false 157 } 158 159 // Expected size for the response is around 6 kilobytes. 160 if resp.ContentLength > 20*1024 { 161 log.Infof("Filesize is too large: %d bytes", resp.ContentLength) 162 return false 163 } 164 165 bodyBytes, err := io.ReadAll(resp.Body) 166 if err != nil { 167 log.Infof("Error reading response body: %v", err) 168 return false 169 } 170 171 bodyString := string(bodyBytes) 172 if !strings.Contains(bodyString, "File Browser") { 173 log.Infof("Response body does not contain 'File Browser'") 174 return false 175 } 176 177 return true 178 } 179 180 // checkLogin checks if the login with default credentials is successful. 181 func checkLogin(ctx context.Context, fileBrowserIP string, fileBrowserPort int) bool { 182 client := &http.Client{Timeout: requestTimeout} 183 targetURL := fmt.Sprintf("http://%s/api/login", net.JoinHostPort(fileBrowserIP, strconv.Itoa(fileBrowserPort))) 184 185 //nolint:errchkjson // this is a static struct, so it cannot fail 186 requestBody, _ := json.Marshal(map[string]string{ 187 "username": "admin", 188 "password": "admin", 189 "recaptcha": "", 190 }) 191 192 req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, io.NopCloser(bytes.NewBuffer(requestBody))) 193 if err != nil { 194 log.Infof("Error while constructing request %s to the server: %v", targetURL, err) 195 return false 196 } 197 req.Header.Set("Content-Type", "application/json") 198 199 resp, err := client.Do(req) 200 if err != nil { 201 if errors.Is(err, context.DeadlineExceeded) { 202 log.Infof("Timeout exceeded when accessing %s", targetURL) 203 } else { 204 log.Infof("Error when sending request %s to the server: %v", targetURL, err) 205 } 206 return false 207 } 208 defer resp.Body.Close() 209 210 return resp.StatusCode == http.StatusOK 211 }