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  }