Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit 4a6d6c4

Browse files
MacJediWizardclaude
andcommitted
security: Fix shell injection and TOCTOU vulnerabilities
- Fix #17: Remove shell interpretation in restart helpers by using direct nohup execution - Fix #18: Add TOCTOU race condition mitigations: - Use O_EXCL flag for atomic file creation - Generate random filename suffixes to prevent predictable paths - Verify directory is not a symlink before creating helper scripts - Verify file is not a symlink before execution - Add oscap-docker installation when Docker is enabled after Compliance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8feb9ff commit 4a6d6c4

2 files changed

Lines changed: 239 additions & 76 deletions

File tree

cmd/patchmon-agent/commands/serve.go

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package commands
22

33
import (
44
"context"
5+
"crypto/rand"
56
"crypto/tls"
7+
"encoding/hex"
68
"encoding/json"
79
"fmt"
810
"net/http"
911
"os"
1012
"os/exec"
13+
"path/filepath"
1114
"strings"
15+
"syscall"
1216
"time"
1317

1418
"patchmon-agent/internal/client"
@@ -996,42 +1000,69 @@ func toggleIntegration(integrationName string, enabled bool) error {
9961000
}
9971001
}
9981002

999-
// Handle Docker Bench installation when Docker is enabled AND Compliance is already enabled
1003+
// Handle Docker Bench and oscap-docker installation when Docker is enabled AND Compliance is already enabled
10001004
if integrationName == "docker" && enabled {
10011005
if cfgManager.IsIntegrationEnabled("compliance") {
1002-
logger.Info("Docker enabled with Compliance already active - setting up Docker Bench...")
1006+
logger.Info("Docker enabled with Compliance already active - setting up Docker scanning tools...")
10031007
httpClient := client.New(cfgManager, logger)
10041008
ctx := context.Background()
10051009

1010+
openscapScanner := compliance.NewOpenSCAPScanner(logger)
1011+
scannerDetails := openscapScanner.GetScannerDetails()
1012+
1013+
// Setup Docker Bench
10061014
dockerBenchScanner := compliance.NewDockerBenchScanner(logger)
10071015
if dockerBenchScanner.IsAvailable() {
10081016
if err := dockerBenchScanner.EnsureInstalled(); err != nil {
10091017
logger.WithError(err).Warn("Failed to pre-pull Docker Bench image (will pull on first scan)")
10101018
} else {
10111019
logger.Info("Docker Bench image pulled successfully")
1012-
1013-
// Update compliance status to include Docker Bench
1014-
openscapScanner := compliance.NewOpenSCAPScanner(logger)
1015-
scannerDetails := openscapScanner.GetScannerDetails()
10161020
scannerDetails.DockerBenchAvailable = true
10171021
scannerDetails.AvailableProfiles = append(scannerDetails.AvailableProfiles, models.ScanProfileInfo{
10181022
ID: "docker-bench",
10191023
Name: "Docker Bench for Security",
10201024
Description: "CIS Docker Benchmark security checks",
10211025
Type: "docker-bench",
10221026
})
1027+
}
1028+
} else {
1029+
logger.Warn("Docker daemon not available - Docker Bench cannot be used")
1030+
}
10231031

1024-
httpClient.SendIntegrationSetupStatus(ctx, &models.IntegrationSetupStatus{
1025-
Integration: "compliance",
1026-
Enabled: true,
1027-
Status: "ready",
1028-
Message: "Docker Bench now available",
1029-
ScannerInfo: scannerDetails,
1032+
// Setup oscap-docker for container image CVE scanning
1033+
oscapDockerScanner := compliance.NewOscapDockerScanner(logger)
1034+
if !oscapDockerScanner.IsAvailable() {
1035+
if err := oscapDockerScanner.EnsureInstalled(); err != nil {
1036+
logger.WithError(err).Warn("Failed to install oscap-docker (container CVE scanning won't be available)")
1037+
} else {
1038+
logger.Info("oscap-docker installed successfully")
1039+
scannerDetails.AvailableProfiles = append(scannerDetails.AvailableProfiles, models.ScanProfileInfo{
1040+
ID: "docker-image-cve",
1041+
Name: "Docker Image CVE Scan",
1042+
Description: "Scan Docker images for known CVEs using OpenSCAP",
1043+
Type: "oscap-docker",
1044+
Category: "docker",
10301045
})
10311046
}
10321047
} else {
1033-
logger.Warn("Docker daemon not available - Docker Bench cannot be used")
1048+
logger.Info("oscap-docker already available")
1049+
scannerDetails.AvailableProfiles = append(scannerDetails.AvailableProfiles, models.ScanProfileInfo{
1050+
ID: "docker-image-cve",
1051+
Name: "Docker Image CVE Scan",
1052+
Description: "Scan Docker images for known CVEs using OpenSCAP",
1053+
Type: "oscap-docker",
1054+
Category: "docker",
1055+
})
10341056
}
1057+
1058+
// Send updated compliance status with Docker scanning tools
1059+
httpClient.SendIntegrationSetupStatus(ctx, &models.IntegrationSetupStatus{
1060+
Integration: "compliance",
1061+
Enabled: true,
1062+
Status: "ready",
1063+
Message: "Docker scanning tools now available",
1064+
ScannerInfo: scannerDetails,
1065+
})
10351066
}
10361067
}
10371068

@@ -1071,10 +1102,12 @@ func toggleIntegration(integrationName string, enabled bool) error {
10711102
}
10721103

10731104
// Create a helper script that will restart the service after we exit
1074-
// SECURITY NOTE: Writing scripts to disk has a TOCTOU race condition risk.
1075-
// Mitigations: 1) 0700 permissions on dir and file (owner-only)
1076-
// 2) Script is deleted immediately after execution
1077-
// 3) Short window between write and exec (milliseconds)
1105+
// SECURITY: TOCTOU mitigation measures:
1106+
// 1) Use random suffix to prevent predictable paths
1107+
// 2) Use O_EXCL flag for atomic creation (fail if file exists)
1108+
// 3) 0700 permissions on dir and file (owner-only)
1109+
// 4) Script is deleted immediately after execution
1110+
// 5) Verify no symlink attacks before execution
10781111
helperScript := `#!/bin/sh
10791112
# Wait a moment for the current process to exit
10801113
sleep 2
@@ -1083,28 +1116,69 @@ rc-service patchmon-agent restart 2>&1 || rc-service patchmon-agent start 2>&1
10831116
# Clean up this script
10841117
rm -f "$0"
10851118
`
1086-
helperPath := "/etc/patchmon/patchmon-restart-helper.sh"
1087-
// SECURITY: Use 0700 permissions (owner-only executable) to minimize TOCTOU risk
1088-
if err := os.WriteFile(helperPath, []byte(helperScript), 0700); err != nil {
1119+
// Generate random suffix to prevent predictable path attacks
1120+
randomBytes := make([]byte, 8)
1121+
if _, err := rand.Read(randomBytes); err != nil {
1122+
logger.WithError(err).Warn("Failed to generate random suffix, using fallback")
1123+
randomBytes = []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
1124+
}
1125+
helperPath := filepath.Join("/etc/patchmon", fmt.Sprintf("restart-%s.sh", hex.EncodeToString(randomBytes)))
1126+
1127+
// SECURITY: Verify the directory is not a symlink (prevent symlink attacks)
1128+
dirInfo, err := os.Lstat("/etc/patchmon")
1129+
if err == nil && dirInfo.Mode()&os.ModeSymlink != 0 {
1130+
logger.Warn("Security: /etc/patchmon is a symlink, refusing to create helper script")
1131+
os.Exit(0) // Fall through to exit approach
1132+
}
1133+
1134+
// SECURITY: Use O_EXCL to atomically create file (fail if exists - prevents race conditions)
1135+
file, err := os.OpenFile(helperPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700)
1136+
if err != nil {
10891137
logger.WithError(err).Warn("Failed to create restart helper script, will exit and rely on OpenRC auto-restart")
10901138
// Fall through to exit approach
10911139
} else {
1092-
// Execute the helper script in background (detached from current process)
1093-
// Use 'sh -c' with nohup to ensure it runs after we exit
1094-
cmd := exec.Command("sh", "-c", fmt.Sprintf("nohup %s > /dev/null 2>&1 &", helperPath))
1095-
if err := cmd.Start(); err != nil {
1096-
logger.WithError(err).Warn("Failed to start restart helper script, will exit and rely on OpenRC auto-restart")
1097-
// Clean up script
1098-
if removeErr := os.Remove(helperPath); removeErr != nil {
1099-
logger.WithError(removeErr).Debug("Failed to remove helper script")
1100-
}
1140+
// Write the script content to the file
1141+
if _, err := file.WriteString(helperScript); err != nil {
1142+
logger.WithError(err).Warn("Failed to write restart helper script")
1143+
file.Close()
1144+
os.Remove(helperPath)
11011145
// Fall through to exit approach
11021146
} else {
1103-
logger.Info("Scheduled service restart via helper script, exiting now...")
1104-
// Give the helper script a moment to start
1105-
time.Sleep(500 * time.Millisecond)
1106-
// Exit gracefully - the helper script will restart the service
1107-
os.Exit(0)
1147+
file.Close()
1148+
1149+
// SECURITY: Verify the file we're about to execute is the one we created
1150+
// Check it's a regular file, not a symlink that was swapped in
1151+
fileInfo, err := os.Lstat(helperPath)
1152+
if err != nil || fileInfo.Mode()&os.ModeSymlink != 0 {
1153+
logger.Warn("Security: helper script may have been tampered with, refusing to execute")
1154+
os.Remove(helperPath)
1155+
os.Exit(0)
1156+
}
1157+
1158+
// Execute the helper script in background (detached from current process)
1159+
// SECURITY: Avoid shell interpretation by executing directly with nohup
1160+
cmd := exec.Command("nohup", helperPath)
1161+
cmd.Stdout = nil
1162+
cmd.Stderr = nil
1163+
// Detach from parent process group to ensure script continues after we exit
1164+
cmd.SysProcAttr = &syscall.SysProcAttr{
1165+
Setpgid: true,
1166+
Pgid: 0,
1167+
}
1168+
if err := cmd.Start(); err != nil {
1169+
logger.WithError(err).Warn("Failed to start restart helper script, will exit and rely on OpenRC auto-restart")
1170+
// Clean up script
1171+
if removeErr := os.Remove(helperPath); removeErr != nil {
1172+
logger.WithError(removeErr).Debug("Failed to remove helper script")
1173+
}
1174+
// Fall through to exit approach
1175+
} else {
1176+
logger.Info("Scheduled service restart via helper script, exiting now...")
1177+
// Give the helper script a moment to start
1178+
time.Sleep(500 * time.Millisecond)
1179+
// Exit gracefully - the helper script will restart the service
1180+
os.Exit(0)
1181+
}
11081182
}
11091183
}
11101184

0 commit comments

Comments
 (0)