From cd73e3ef9bee3c53d60944459a5ec1219ee3dd85 Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:30:23 -0500 Subject: [PATCH 1/8] sec: added security scan #38 --- .github/workflows/benchmark_cmd_app.yml | 2 +- .github/workflows/build_cmd_app.yml | 4 ++- .github/workflows/scan_cmd_app.yml | 39 +++++++++++++++++++++++++ .github/workflows/test_cmd_app.yml | 4 ++- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/scan_cmd_app.yml diff --git a/.github/workflows/benchmark_cmd_app.yml b/.github/workflows/benchmark_cmd_app.yml index c9c3b73..d58af1c 100644 --- a/.github/workflows/benchmark_cmd_app.yml +++ b/.github/workflows/benchmark_cmd_app.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version-file: 'go.mod' - name: Download dependencies run: go mod download diff --git a/.github/workflows/build_cmd_app.yml b/.github/workflows/build_cmd_app.yml index d7e5768..2a5e99a 100644 --- a/.github/workflows/build_cmd_app.yml +++ b/.github/workflows/build_cmd_app.yml @@ -8,6 +8,8 @@ jobs: build: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - uses: actions/checkout@v5 @@ -15,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version-file: 'go.mod' - name: Download dependencies run: go mod download diff --git a/.github/workflows/scan_cmd_app.yml b/.github/workflows/scan_cmd_app.yml new file mode 100644 index 0000000..8859679 --- /dev/null +++ b/.github/workflows/scan_cmd_app.yml @@ -0,0 +1,39 @@ +name: Security Scan +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + security: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout Source + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + args: '-fmt sarif -out gosec.sarif ./...' + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: gosec.sarif + + - name: Run govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... diff --git a/.github/workflows/test_cmd_app.yml b/.github/workflows/test_cmd_app.yml index e21ee9f..4e5f9a3 100644 --- a/.github/workflows/test_cmd_app.yml +++ b/.github/workflows/test_cmd_app.yml @@ -8,6 +8,8 @@ jobs: test: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - uses: actions/checkout@v5 @@ -15,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version-file: 'go.mod' - name: Download dependencies run: go mod download From 4b606c4c4c7793ec8b57bc00deb3bbd107cfd645 Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:37:26 -0500 Subject: [PATCH 2/8] sec: escape event string #38 --- cmd/app/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/app/main.go b/cmd/app/main.go index 26aa63f..2f48e82 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "html" "html/template" "log/slog" "os" @@ -84,7 +85,8 @@ func main() { htmxFuncs := template.FuncMap{ "preload": func(event string) template.HTMLAttr { - return template.HTMLAttr(fmt.Sprintf(`preload="%s"`, event)) + escaped := html.EscapeString(event) + return template.HTMLAttr(fmt.Sprintf(`preload="%s"`, escaped)) }, } From 2bd542249b6ce397b8c9d0eff310bea1f5b11daa Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:40:26 -0500 Subject: [PATCH 3/8] sec: intentionally ignore errors to prevent timing attacks #38 --- cmd/app/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/app/handlers.go b/cmd/app/handlers.go index c8d5c11..c6f985f 100644 --- a/cmd/app/handlers.go +++ b/cmd/app/handlers.go @@ -168,7 +168,7 @@ func (app *app) postSignIn(w http.ResponseWriter, r *http.Request) { // Prevent timing attacks - use pre-computed bcrypt hash dummyUser := &users.User{} dummyUser.Password = users.Password{Hash: []byte(users.PreComputedHash)} - dummyUser.Password.Matches(form.Password) + _, _ = dummyUser.Password.Matches(form.Password) // Intentionally ignored to prevent timing attacks match = false app.logger.WarnContext(r.Context(), "error getting user by email", slog.String("msg", err.Error())) From b76f2aacc463474d93b3ef90e689fa4ce58ed24a Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:42:39 -0500 Subject: [PATCH 4/8] sec: handle buffer write to error #38 --- cmd/app/helpers.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/app/helpers.go b/cmd/app/helpers.go index 54ae6cf..bcf079b 100644 --- a/cmd/app/helpers.go +++ b/cmd/app/helpers.go @@ -37,7 +37,9 @@ func (app *app) render(w http.ResponseWriter, r *http.Request, status int, templ w.WriteHeader(status) - buf.WriteTo(w) + if _, err := buf.WriteTo(w); err != nil { + app.logger.ErrorContext(r.Context(), "failed to write response", slog.String("msg", err.Error())) + } } func (app *app) renderError(w http.ResponseWriter, r *http.Request, err error, userMessage string) { From 2f8dd25819e0575f54b1f44111f514c6c25f4811 Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:44:22 -0500 Subject: [PATCH 5/8] sec: ignore db close error #38 --- internal/data/sqlite.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/data/sqlite.go b/internal/data/sqlite.go index c2b1c52..a1e9253 100644 --- a/internal/data/sqlite.go +++ b/internal/data/sqlite.go @@ -24,7 +24,7 @@ func (p *SQLiteProvider) Open(path string) (*sql.DB, error) { defer cancel() err = db.PingContext(ctx) if err != nil { - db.Close() + _ = db.Close() // Ignore close error, ping error takes precedence return nil, err } From dbf804e7862d9ac320528dd48dab645ce088b03a Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:46:10 -0500 Subject: [PATCH 6/8] sec: handle crypto rand error #38 --- cmd/app/helpers.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/app/helpers.go b/cmd/app/helpers.go index bcf079b..f795025 100644 --- a/cmd/app/helpers.go +++ b/cmd/app/helpers.go @@ -146,7 +146,9 @@ func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error { func generateTraceID() string { bytes := make([]byte, 4) - rand.Read(bytes) + if _, err := rand.Read(bytes); err != nil { + panic(err) // crypto/rand should never fail + } return hex.EncodeToString(bytes) } From 88a5cb1e25c331d81103314a6472f46bd903751a Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sun, 9 Nov 2025 12:54:22 -0500 Subject: [PATCH 7/8] sec: remove htmx functions from server side #38 --- cmd/app/main.go | 10 +--------- cmd/app/testutils_test.go | 9 +-------- ui/html/app/goals.html | 10 +++++----- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/cmd/app/main.go b/cmd/app/main.go index 2f48e82..f194ee6 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "html" "html/template" "log/slog" "os" @@ -83,14 +82,7 @@ func main() { logger.Info("database version", slog.Int64("version", *dbVersion)) - htmxFuncs := template.FuncMap{ - "preload": func(event string) template.HTMLAttr { - escaped := html.EscapeString(event) - return template.HTMLAttr(fmt.Sprintf(`preload="%s"`, escaped)) - }, - } - - templateCache, err := newTemplateCache(WithFunctions(htmxFuncs)) + templateCache, err := newTemplateCache() if err != nil { logger.Error("error creating template cache", slog.String("msg", err.Error())) os.Exit(1) diff --git a/cmd/app/testutils_test.go b/cmd/app/testutils_test.go index 7e5f342..1d44066 100644 --- a/cmd/app/testutils_test.go +++ b/cmd/app/testutils_test.go @@ -2,8 +2,6 @@ package main import ( "bytes" - "fmt" - "html/template" "io" "log/slog" "net/http" @@ -36,12 +34,7 @@ func newTestApplication(tb testing.TB) *app { tb.Fatal(err) } - // Create an instance of the template cache with HTMX functions. - htmxFuncs := template.FuncMap{ - "get": func(url string) template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`hx-get="%s"`, url)) }, - "preload": func(event string) template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`preload="%s"`, event)) }, - } - templateCache, err := newTemplateCache(WithFunctions(htmxFuncs)) + templateCache, err := newTemplateCache() if err != nil { tb.Fatal(err) } diff --git a/ui/html/app/goals.html b/ui/html/app/goals.html index 0a270ed..7b3715e 100644 --- a/ui/html/app/goals.html +++ b/ui/html/app/goals.html @@ -23,7 +23,7 @@

{{ if eq (mod $index 2) 0 }} {{ $goal.Due.Format "2006" }}: @@ -67,7 +67,7 @@

{{ $goal.Due.Format "2006" }}: {{ $goal.Goal }} @@ -85,7 +85,7 @@