github.com/google/osv-scalibr@v0.4.1/detector/misc/dockersocket/dockersocket.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  //go:build !windows
    16  
    17  // Package dockersocket implements a detector for Docker socket exposure vulnerabilities.
    18  package dockersocket
    19  
    20  import (
    21  	"bufio"
    22  	"context"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/fs"
    28  	"os"
    29  	"strings"
    30  	"syscall"
    31  
    32  	"github.com/google/osv-scalibr/detector"
    33  	scalibrfs "github.com/google/osv-scalibr/fs"
    34  	"github.com/google/osv-scalibr/inventory"
    35  	"github.com/google/osv-scalibr/packageindex"
    36  	"github.com/google/osv-scalibr/plugin"
    37  )
    38  
    39  const (
    40  	// Name of the detector.
    41  	Name = "dockersocket"
    42  )
    43  
    44  // Detector is a SCALIBR Detector for Docker socket exposure vulnerabilities.
    45  type Detector struct{}
    46  
    47  // New returns a detector.
    48  func New() detector.Detector {
    49  	return &Detector{}
    50  }
    51  
    52  // Name of the detector.
    53  func (Detector) Name() string { return Name }
    54  
    55  // Version of the detector.
    56  func (Detector) Version() int { return 0 }
    57  
    58  // RequiredExtractors returns an empty list as there are no dependencies.
    59  func (Detector) RequiredExtractors() []string { return []string{} }
    60  
    61  // Requirements of the Detector.
    62  func (Detector) Requirements() *plugin.Capabilities { return &plugin.Capabilities{OS: plugin.OSUnix} }
    63  
    64  // Scan starts the scan.
    65  func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) {
    66  	return d.ScanFS(ctx, scanRoot.FS, px)
    67  }
    68  
    69  // DetectedFinding returns generic vulnerability information about what is detected.
    70  func (d Detector) DetectedFinding() inventory.Finding {
    71  	return d.findingForTarget(nil)
    72  }
    73  
    74  func (Detector) findingForTarget(target *inventory.GenericFindingTargetDetails) inventory.Finding {
    75  	return inventory.Finding{GenericFindings: []*inventory.GenericFinding{{
    76  		Adv: &inventory.GenericFindingAdvisory{
    77  			ID: &inventory.AdvisoryID{
    78  				Publisher: "SCALIBR",
    79  				Reference: "docker-socket-exposure",
    80  			},
    81  			Title: "Docker Socket Exposure Detection",
    82  			Description: "Docker socket exposure can lead to privilege escalation and container escape vulnerabilities. " +
    83  				"Insecure Docker socket permissions, daemon configuration, or systemd service settings " +
    84  				"may allow unauthorized access to the Docker API, potentially compromising the entire host system.",
    85  			Recommendation: "Secure Docker socket by: 1) Setting appropriate file permissions (660) on /var/run/docker.sock, " +
    86  				"2) Configuring daemon.json to use TLS authentication for remote API access, " +
    87  				"3) Ensuring systemd service configurations use secure API bindings with proper authentication.",
    88  			Sev: inventory.SeverityHigh,
    89  		},
    90  		Target: target,
    91  	}}}
    92  }
    93  
    94  // ScanFS starts the scan from a pseudo-filesystem.
    95  func (d Detector) ScanFS(ctx context.Context, fsys fs.FS, px *packageindex.PackageIndex) (inventory.Finding, error) {
    96  	var issues []string
    97  
    98  	// Check for context timeout
    99  	if ctx.Err() != nil {
   100  		return inventory.Finding{}, ctx.Err()
   101  	}
   102  
   103  	// Check Docker socket file permissions
   104  	if socketIssues := d.checkDockerSocketPermissions(fsys); len(socketIssues) > 0 {
   105  		issues = append(issues, socketIssues...)
   106  	}
   107  
   108  	// Check for context timeout
   109  	if ctx.Err() != nil {
   110  		return inventory.Finding{}, ctx.Err()
   111  	}
   112  
   113  	// Check Docker daemon configuration
   114  	if daemonIssues := d.checkDockerDaemonConfig(ctx, fsys); len(daemonIssues) > 0 {
   115  		issues = append(issues, daemonIssues...)
   116  	}
   117  
   118  	// Check for context timeout
   119  	if ctx.Err() != nil {
   120  		return inventory.Finding{}, ctx.Err()
   121  	}
   122  
   123  	// Check systemd service configuration
   124  	if systemdIssues := d.checkSystemdServiceConfig(ctx, fsys); len(systemdIssues) > 0 {
   125  		issues = append(issues, systemdIssues...)
   126  	}
   127  
   128  	if len(issues) == 0 {
   129  		return inventory.Finding{}, nil
   130  	}
   131  
   132  	target := &inventory.GenericFindingTargetDetails{Extra: strings.Join(issues, "; ")}
   133  	return d.findingForTarget(target), nil
   134  }
   135  
   136  // checkDockerSocketPermissions checks /var/run/docker.sock for insecure permissions.
   137  func (d Detector) checkDockerSocketPermissions(fsys fs.FS) []string {
   138  	var issues []string
   139  
   140  	f, err := fsys.Open("var/run/docker.sock")
   141  	if err != nil {
   142  		if errors.Is(err, os.ErrNotExist) {
   143  			// Socket doesn't exist, Docker likely not installed - no issue
   144  			return issues
   145  		}
   146  		// Cannot access socket - potential permission issue but can't verify
   147  		return issues
   148  	}
   149  	defer f.Close()
   150  
   151  	info, err := f.Stat()
   152  	if err != nil {
   153  		return issues
   154  	}
   155  
   156  	// Check if socket is world-readable or world-writable
   157  	perms := info.Mode().Perm()
   158  	if perms&0004 != 0 {
   159  		issues = append(issues, fmt.Sprintf("Docker socket is world-readable (permissions: %03o)", perms))
   160  	}
   161  	if perms&0002 != 0 {
   162  		issues = append(issues, fmt.Sprintf("Docker socket is world-writable (permissions: %03o)", perms))
   163  	}
   164  
   165  	// Check ownership
   166  	stat, ok := info.Sys().(*syscall.Stat_t)
   167  	if ok {
   168  		if stat.Uid != 0 {
   169  			issues = append(issues, fmt.Sprintf("Docker socket owner is not root (uid: %d)", stat.Uid))
   170  		}
   171  		// Note: Group ownership of 'docker' (typically GID varies) is acceptable
   172  	}
   173  
   174  	return issues
   175  }
   176  
   177  // dockerDaemonConfig represents the structure of /etc/docker/daemon.json.
   178  type dockerDaemonConfig struct {
   179  	Hosts []string `json:"hosts"`
   180  }
   181  
   182  // checkDockerDaemonConfig checks /etc/docker/daemon.json for insecure host configurations.
   183  func (d Detector) checkDockerDaemonConfig(ctx context.Context, fsys fs.FS) []string {
   184  	var issues []string
   185  
   186  	f, err := fsys.Open("etc/docker/daemon.json")
   187  	if err != nil {
   188  		if errors.Is(err, os.ErrNotExist) {
   189  			// Config file doesn't exist - no issue to report
   190  			return issues
   191  		}
   192  		return issues
   193  	}
   194  	defer f.Close()
   195  
   196  	content, err := io.ReadAll(f)
   197  	if err != nil {
   198  		return issues
   199  	}
   200  
   201  	var config dockerDaemonConfig
   202  	if err := json.Unmarshal(content, &config); err != nil {
   203  		// Invalid JSON - potential issue but not our concern
   204  		return issues
   205  	}
   206  
   207  	// Check for insecure host bindings
   208  	for _, host := range config.Hosts {
   209  		// Check for context timeout
   210  		if ctx.Err() != nil {
   211  			return issues
   212  		}
   213  		if strings.HasPrefix(host, "tcp://") {
   214  			// TCP binding without TLS - potential security issue
   215  			issues = append(issues, fmt.Sprintf("Insecure TCP binding in daemon.json: %q (consider using TLS)", host))
   216  		}
   217  	}
   218  
   219  	return issues
   220  }
   221  
   222  // checkSystemdServiceConfig checks Docker systemd service files for insecure configurations.
   223  func (d Detector) checkSystemdServiceConfig(ctx context.Context, fsys fs.FS) []string {
   224  	var issues []string
   225  
   226  	// Check common systemd service locations
   227  	servicePaths := []string{
   228  		"etc/systemd/system/docker.service",
   229  		"lib/systemd/system/docker.service",
   230  		"usr/lib/systemd/system/docker.service",
   231  	}
   232  
   233  	for _, path := range servicePaths {
   234  		// Check for context timeout
   235  		if ctx.Err() != nil {
   236  			return issues
   237  		}
   238  		if serviceIssues := d.checkSystemdServiceFile(ctx, fsys, path); len(serviceIssues) > 0 {
   239  			issues = append(issues, serviceIssues...)
   240  		}
   241  	}
   242  
   243  	return issues
   244  }
   245  
   246  // checkSystemdServiceFile checks a specific systemd service file for insecure Docker configurations.
   247  func (d Detector) checkSystemdServiceFile(ctx context.Context, fsys fs.FS, path string) []string {
   248  	var issues []string
   249  
   250  	f, err := fsys.Open(path)
   251  	if err != nil {
   252  		if errors.Is(err, os.ErrNotExist) {
   253  			// Service file doesn't exist - no issue
   254  			return issues
   255  		}
   256  		return issues
   257  	}
   258  	defer f.Close()
   259  
   260  	scanner := bufio.NewScanner(f)
   261  	for scanner.Scan() {
   262  		// Check for context timeout
   263  		if ctx.Err() != nil {
   264  			return issues
   265  		}
   266  
   267  		line := strings.TrimSpace(scanner.Text())
   268  		if strings.HasPrefix(line, "ExecStart=") {
   269  			// Check for insecure -H tcp:// flags in ExecStart
   270  			if strings.Contains(line, "-H tcp://") && !strings.Contains(line, "--tls") {
   271  				issues = append(issues, fmt.Sprintf("Insecure TCP binding in %q: %q (missing TLS)", path, line))
   272  			}
   273  		}
   274  	}
   275  
   276  	if err := scanner.Err(); err != nil {
   277  		return issues
   278  	}
   279  
   280  	return issues
   281  }