fix: address bugs and security gaps found in code audit round 2
- Fix blocking MikroTik channel sends that could leak goroutines - Default to 10s timeout when check TimeoutMs is 0 - Add payload size limit to check_jobs event - Add 30s write deadline to WebSocket writes - Use random ICMP sequence numbers to prevent ping ID collisions - Atomic host key file writes via temp+rename - Sanitize update URLs in log output - Zero correct buffer in sendBinaryResult after MarshalAppend
This commit is contained in:
parent
b4f3dabe8d
commit
08d5957690
7 changed files with 52 additions and 9 deletions
9
agent.go
9
agent.go
|
|
@ -150,6 +150,9 @@ func runSession(ctx context.Context, baseURL, token string) error {
|
|||
return
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(bin)
|
||||
// Zero the serialized protobuf data (may contain credentials).
|
||||
// MarshalAppend may return a new backing array, so zero both.
|
||||
zeroBytes(bin)
|
||||
full := (*bp)[:cap(*bp)]
|
||||
zeroBytes(full)
|
||||
*bp = full[:0]
|
||||
|
|
@ -391,6 +394,10 @@ func handleMessage(
|
|||
slog.Error("decode check_jobs payload", "error", err)
|
||||
return false
|
||||
}
|
||||
if len(payload.Binary) > maxJobPayloadBytes {
|
||||
slog.Error("check_jobs payload too large", "size", len(payload.Binary), "max", maxJobPayloadBytes)
|
||||
return false
|
||||
}
|
||||
bin, err := base64.StdEncoding.DecodeString(payload.Binary)
|
||||
if err != nil {
|
||||
slog.Error("decode check_jobs base64", "error", err)
|
||||
|
|
@ -419,7 +426,7 @@ func handleMessage(
|
|||
slog.Error("invalid update payload")
|
||||
return false
|
||||
}
|
||||
slog.Info("update requested", "url", payload.URL)
|
||||
slog.Info("update requested", "url", sanitizeURL(payload.URL))
|
||||
if err := doSelfUpdate(payload.URL, payload.Checksum); err != nil {
|
||||
slog.Error("self-update failed", "error", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ func ExecuteCheck(ctx context.Context, check *pb.Check) *pb.CheckResult {
|
|||
|
||||
// executeHTTPCheck performs an HTTP/HTTPS check
|
||||
func executeHTTPCheck(ctx context.Context, config *pb.HttpCheckConfig, timeoutMs uint32) (uint32, string, float64) {
|
||||
if timeoutMs == 0 {
|
||||
timeoutMs = 10000
|
||||
}
|
||||
timeout := time.Duration(timeoutMs) * time.Millisecond
|
||||
|
||||
// Create HTTP client with timeout and TLS config
|
||||
|
|
@ -141,6 +144,9 @@ func executeHTTPCheck(ctx context.Context, config *pb.HttpCheckConfig, timeoutMs
|
|||
|
||||
// executeTCPCheck performs a TCP port connectivity check
|
||||
func executeTCPCheck(ctx context.Context, config *pb.TcpCheckConfig, timeoutMs uint32) (uint32, string, float64) {
|
||||
if timeoutMs == 0 {
|
||||
timeoutMs = 10000
|
||||
}
|
||||
timeout := time.Duration(timeoutMs) * time.Millisecond
|
||||
address := net.JoinHostPort(config.Host, strconv.Itoa(int(config.Port)))
|
||||
|
||||
|
|
@ -181,6 +187,9 @@ func executeTCPCheck(ctx context.Context, config *pb.TcpCheckConfig, timeoutMs u
|
|||
|
||||
// executeDNSCheck performs a DNS resolution check
|
||||
func executeDNSCheck(ctx context.Context, config *pb.DnsCheckConfig, timeoutMs uint32) (uint32, string, float64) {
|
||||
if timeoutMs == 0 {
|
||||
timeoutMs = 10000
|
||||
}
|
||||
timeout := time.Duration(timeoutMs) * time.Millisecond
|
||||
|
||||
resolver := &net.Resolver{}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ func (s *hostKeyStore) save() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.path, data, 0600)
|
||||
tmp := s.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, s.path)
|
||||
}
|
||||
|
||||
// verify checks a fingerprint for host. Returns nil on match or first-use, error on mismatch.
|
||||
|
|
|
|||
24
mikrotik.go
24
mikrotik.go
|
|
@ -281,11 +281,15 @@ func executeMikrotikJob(ctx context.Context, job *pb.AgentJob, resultCh chan<- *
|
|||
|
||||
client, err := mikrotikDial(dev.Ip, dev.Port, dev.Username, dev.Password, dev.UseSsl)
|
||||
if err != nil {
|
||||
resultCh <- &pb.MikrotikResult{
|
||||
select {
|
||||
case resultCh <- &pb.MikrotikResult{
|
||||
DeviceId: job.DeviceId,
|
||||
JobId: job.JobId,
|
||||
Error: fmt.Sprintf("connection failed: %v", err),
|
||||
Timestamp: timestamp,
|
||||
}:
|
||||
default:
|
||||
slog.Warn("result channel full", "job_id", job.JobId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -314,12 +318,16 @@ func executeMikrotikJob(ctx context.Context, job *pb.AgentJob, resultCh chan<- *
|
|||
}
|
||||
}
|
||||
|
||||
resultCh <- &pb.MikrotikResult{
|
||||
select {
|
||||
case resultCh <- &pb.MikrotikResult{
|
||||
DeviceId: job.DeviceId,
|
||||
JobId: job.JobId,
|
||||
Sentences: allSentences,
|
||||
Error: errorMessage,
|
||||
Timestamp: timestamp,
|
||||
}:
|
||||
default:
|
||||
slog.Warn("result channel full", "job_id", job.JobId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,21 +337,29 @@ func executeMikrotikBackupViaSSH(job *pb.AgentJob, dev *pb.MikrotikDevice, resul
|
|||
|
||||
config, err := sshBackup(dev.Ip, uint16(dev.SshPort), dev.Username, dev.Password)
|
||||
if err != nil {
|
||||
resultCh <- &pb.MikrotikResult{
|
||||
select {
|
||||
case resultCh <- &pb.MikrotikResult{
|
||||
DeviceId: job.DeviceId,
|
||||
JobId: job.JobId,
|
||||
Error: fmt.Sprintf("SSH backup failed: %v", err),
|
||||
Timestamp: timestamp,
|
||||
}:
|
||||
default:
|
||||
slog.Warn("result channel full", "job_id", job.JobId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resultCh <- &pb.MikrotikResult{
|
||||
select {
|
||||
case resultCh <- &pb.MikrotikResult{
|
||||
DeviceId: job.DeviceId,
|
||||
JobId: job.JobId,
|
||||
Sentences: []*pb.MikrotikSentence{
|
||||
{Attributes: map[string]string{"config": config}},
|
||||
},
|
||||
Timestamp: timestamp,
|
||||
}:
|
||||
default:
|
||||
slog.Warn("result channel full", "job_id", job.JobId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
ping.go
6
ping.go
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -72,12 +73,13 @@ func doICMPPing(ip net.IP, network string, isIPv4 bool, timeoutMs int) (float64,
|
|||
}
|
||||
|
||||
id := os.Getpid() & 0xffff
|
||||
seq := int(rand.Uint32() & 0xffff)
|
||||
msg := icmp.Message{
|
||||
Type: msgType,
|
||||
Code: 0,
|
||||
Body: &icmp.Echo{
|
||||
ID: id,
|
||||
Seq: 1,
|
||||
Seq: seq,
|
||||
Data: []byte("towerops"),
|
||||
},
|
||||
}
|
||||
|
|
@ -121,7 +123,7 @@ func doICMPPing(ip net.IP, network string, isIPv4 bool, timeoutMs int) (float64,
|
|||
switch rm.Type {
|
||||
case ipv4.ICMPTypeEchoReply, ipv6.ICMPTypeEchoReply:
|
||||
echo, ok := rm.Body.(*icmp.Echo)
|
||||
if !ok || echo.ID != id {
|
||||
if !ok || echo.ID != id || echo.Seq != seq {
|
||||
continue
|
||||
}
|
||||
return float64(elapsed.Microseconds()) / 1000.0, nil
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func selfUpdate(downloadURL, expectedChecksum string) error {
|
|||
return fmt.Errorf("checksum required for update")
|
||||
}
|
||||
|
||||
slog.Info("downloading update", "url", downloadURL)
|
||||
slog.Info("downloading update", "url", sanitizeURL(downloadURL))
|
||||
|
||||
resp, err := httpGet(downloadURL)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ func (ws *WSConn) Close() error {
|
|||
}
|
||||
|
||||
const wsReadTimeout = 90 * time.Second
|
||||
const wsWriteTimeout = 30 * time.Second
|
||||
|
||||
func (ws *WSConn) readFrame() (opcode int, payload []byte, err error) {
|
||||
// Set read deadline to detect dead connections (MEDIUM-4)
|
||||
|
|
@ -261,6 +262,10 @@ func (ws *WSConn) writeFrame(opcode int, payload []byte) error {
|
|||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
if nc, ok := ws.conn.(net.Conn); ok {
|
||||
_ = nc.SetWriteDeadline(time.Now().Add(wsWriteTimeout))
|
||||
}
|
||||
|
||||
length := len(payload)
|
||||
// Single buffer: max header (2+8+4=14) + payload
|
||||
buf := make([]byte, 0, 14+length)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue