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 }