fix: security hardening and code quality

- SQL query mode uses read-only SQLite connection with PRAGMA query_only=ON
- Lua sandbox removes dofile/loadfile/load after OpenBase to block file access
- Plugin manager sorts by priority once at load time; GetPlugins is a plain copy
- Proxy appends [body truncated] marker when body hits size limit
- App startup exits with os.Exit(1) on DB open failure
- tickCmd uses tea.Tick instead of time.Sleep in a goroutine
- ErrMsg with non-nil error shows notification then quits
- DB stores path for use by read-only query connection
- WAL journal mode + NORMAL synchronous set in migrate()
- config.go uses errors.Is(err, os.ErrNotExist)
- main.go uses os.UserHomeDir() and removes racy port pre-check
- findings renderer is cached and rebuilt only on width change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hadi
2026-05-19 13:38:10 +02:00
parent 41c0e489cf
commit 172a77e13b
11 changed files with 88 additions and 37 deletions
+13 -10
View File
@@ -1,6 +1,7 @@
package app
import (
"fmt"
"log"
"os"
"path/filepath"
@@ -31,10 +32,9 @@ const tickInterval = 2 * time.Second
type tickMsg struct{}
func tickCmd() tea.Cmd {
return func() tea.Msg {
time.Sleep(tickInterval)
return tea.Tick(tickInterval, func(time.Time) tea.Msg {
return tickMsg{}
}
})
}
var sidebarEntries = pageRegistry
@@ -94,14 +94,17 @@ func New(broker *intercept.Broker, name, path string) Model {
sidebarState: sidebarState(cfg.TUI.DefaultSidebarState),
}
if d, err := db.Open(path); err == nil {
m.database = d
broker.SetDB(d)
m.history.SetDB(d)
m.replay.SetDB(d)
m.findingsPage.SetDB(d)
mgr.SetDB(d)
d, err := db.Open(path)
if err != nil {
fmt.Fprintf(os.Stderr, "db: %v\n", err)
os.Exit(1)
}
m.database = d
broker.SetDB(d)
m.history.SetDB(d)
m.replay.SetDB(d)
m.findingsPage.SetDB(d)
mgr.SetDB(d)
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
if err := mgr.LoadFromDir(pluginsDir); err != nil {
+10
View File
@@ -104,6 +104,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case proxyPkg.ErrMsg:
if msg.Err != nil {
log.Printf("proxy error: %v", msg.Err)
return m, tea.Batch(
func() tea.Msg {
return notificationsUI.NotificationMsg{
Title: "Proxy Error",
Body: msg.Err.Error(),
Kind: notificationsUI.KindError,
}
},
tea.Quit,
)
}
return m, nil
+23 -8
View File
@@ -28,6 +28,9 @@ type Model struct {
pager paginator.Model
help help.Model
renderer *glamour.TermRenderer
rendererWidth int
width int
height int
}
@@ -77,6 +80,11 @@ func (m *Model) recalcSizes() {
m.bodyViewport.SetWidth(inner)
m.bodyViewport.SetHeight(bodyVH)
if m.rendererWidth != inner {
m.renderer = nil
m.rendererWidth = 0
}
m.refreshListViewport()
m.refreshBody()
}
@@ -110,12 +118,12 @@ func (m *Model) refreshBody() {
return
}
f := m.findings[m.cursor]
rendered := renderMarkdown(f.Description, m.bodyViewport.Width())
rendered := m.renderMarkdownCached(f.Description, m.bodyViewport.Width())
m.bodyViewport.SetContent(rendered)
m.bodyViewport.GotoTop()
}
func renderMarkdown(src string, width int) string {
func (m *Model) renderMarkdownCached(src string, width int) string {
if src == "" {
return style.S.Faint.Render(util.CenterLines("(ㆆ _ ㆆ)", "no description"))
}
@@ -130,14 +138,21 @@ func renderMarkdown(src string, width int) string {
if width < 10 {
width = 80
}
r, err := glamour.NewTermRenderer(
glamour.WithStyles(style.GlamourStyleConfig(config.Global)),
glamour.WithWordWrap(width),
)
if err != nil {
// Rebuild renderer if width changed or not yet built.
if m.renderer == nil || m.rendererWidth != width {
r, err := glamour.NewTermRenderer(
glamour.WithStyles(style.GlamourStyleConfig(config.Global)),
glamour.WithWordWrap(width),
)
if err == nil {
m.renderer = r
m.rendererWidth = width
}
}
if m.renderer == nil {
return buf.String()
}
out, err := r.Render(buf.String())
out, err := m.renderer.Render(buf.String())
if err != nil {
return buf.String()
}
+7 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/anotherhadi/spilltea/internal/db"
"github.com/anotherhadi/spilltea/internal/keys"
"github.com/anotherhadi/spilltea/internal/style"
"github.com/anotherhadi/spilltea/internal/util"
)
type panel int
@@ -62,7 +63,12 @@ func (m Model) CurrentRaw() string {
return m.entries[m.cursor].RequestRaw
}
func (m Model) CurrentScheme() string { return "https" }
func (m Model) CurrentScheme() string {
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
return "https"
}
return util.InferScheme(m.entries[m.cursor].Host)
}
// RefreshCmd returns the appropriate load command given the current search state.
// The app model should call this instead of LoadEntriesCmd directly so that