Security Fixes

This commit is contained in:
Andy Dixon
2026-06-16 09:41:51 +01:00
parent 9bbd9ceccf
commit fcb19c96c8
22 changed files with 1578 additions and 134 deletions
+51
View File
@@ -0,0 +1,51 @@
package rawclient
import (
"bytes"
"testing"
"github.com/andydixon/nexus/internal/models"
)
func TestHistoryIsCappedByEntryCount(t *testing.T) {
client := NewClient(&models.Profile{})
for i := 0; i < maxHistoryEntries+25; i++ {
client.appendHistory(HistoryEntry{Direction: "recv", Data: []byte{byte(i)}})
}
history := client.GetHistory()
if len(history) != maxHistoryEntries {
t.Fatalf("history length = %d, want %d", len(history), maxHistoryEntries)
}
}
func TestHistoryIsCappedByBytes(t *testing.T) {
client := NewClient(&models.Profile{})
chunk := bytes.Repeat([]byte("a"), 1024)
for i := 0; i < (maxHistoryBytes/len(chunk))+10; i++ {
client.appendHistory(HistoryEntry{Direction: "recv", Data: chunk})
}
total := 0
for _, entry := range client.GetHistory() {
total += len(entry.Data)
}
if total > maxHistoryBytes {
t.Fatalf("history bytes = %d, want <= %d", total, maxHistoryBytes)
}
}
func TestGetHistoryReturnsDeepCopy(t *testing.T) {
client := NewClient(&models.Profile{})
client.appendHistory(HistoryEntry{Direction: "recv", Data: []byte("secret")})
history := client.GetHistory()
history[0].Data[0] = 'X'
again := client.GetHistory()
if string(again[0].Data) != "secret" {
t.Fatalf("history was mutated through returned slice: %q", string(again[0].Data))
}
}
+36 -12
View File
@@ -13,21 +13,27 @@ import (
"github.com/andydixon/nexus/internal/models"
)
const (
maxHistoryEntries = 10000
maxHistoryBytes = 10 * 1024 * 1024
)
// Client manages a raw TCP connection.
// It provides binary-safe data handling with support for text and hex views,
// timestamps, direction markers, and payload presets.
type Client struct {
mu sync.Mutex
config *models.Profile
conn net.Conn
connected bool
onData func(data []byte, direction string, timestamp time.Time)
onError func(err error)
onClose func()
ctx context.Context
cancel context.CancelFunc
history []HistoryEntry
historyMu sync.RWMutex
mu sync.Mutex
config *models.Profile
conn net.Conn
connected bool
onData func(data []byte, direction string, timestamp time.Time)
onError func(err error)
onClose func()
ctx context.Context
cancel context.CancelFunc
history []HistoryEntry
historyBytes int
historyMu sync.RWMutex
}
// HistoryEntry records a single sent or received data frame with its timestamp.
@@ -193,7 +199,10 @@ func (c *Client) GetHistory() []HistoryEntry {
defer c.historyMu.RUnlock()
out := make([]HistoryEntry, len(c.history))
copy(out, c.history)
for i, entry := range c.history {
out[i] = entry
out[i].Data = copyBytes(entry.Data)
}
return out
}
@@ -202,6 +211,7 @@ func (c *Client) ClearHistory() {
c.historyMu.Lock()
defer c.historyMu.Unlock()
c.history = nil
c.historyBytes = 0
}
// SaveCapture writes the session history to a file in JSON format.
@@ -271,7 +281,21 @@ func (c *Client) readLoop() {
func (c *Client) appendHistory(entry HistoryEntry) {
c.historyMu.Lock()
defer c.historyMu.Unlock()
if len(entry.Data) > maxHistoryBytes {
entry.Data = copyBytes(entry.Data[len(entry.Data)-maxHistoryBytes:])
}
c.history = append(c.history, entry)
c.historyBytes += len(entry.Data)
for len(c.history) > maxHistoryEntries || c.historyBytes > maxHistoryBytes {
if len(c.history) == 0 {
c.historyBytes = 0
return
}
c.historyBytes -= len(c.history[0].Data)
c.history[0].Data = nil
c.history = c.history[1:]
}
}
// applyLineEnding appends the configured line ending to text.