Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
scale
server
general
logging
requireScope: true
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
Expand Down Expand Up @@ -251,4 +252,4 @@ jobs:
echo "## πŸ“¦ Build Artifact" >> $GITHUB_STEP_SUMMARY
echo "| File | Size |" >> $GITHUB_STEP_SUMMARY
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
ls -lh bin/*.exe | awk '{print "| " $9 " | " $5 " |"}' >> $GITHUB_STEP_SUMMARY
ls -lh bin/*.exe | awk '{print "| " $9 " | " $5 " |"}' >> $GITHUB_STEP_SUMMARY
2 changes: 2 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func (m *Manager) SetSessionCookie(w http.ResponseWriter) string {
Path: "/",
MaxAge: int(SessionDuration.Seconds()),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
return token
Expand All @@ -174,6 +175,7 @@ func (m *Manager) ClearSessionCookie(w http.ResponseWriter) {
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
Expand Down
28 changes: 17 additions & 11 deletions internal/logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@

// FilteredLogger wraps a file writer with verbose filtering
type FilteredLogger struct {
file *os.File
writer io.Writer
mu sync.Mutex
verbose *bool
vMu *sync.RWMutex
}

// NewFilteredLogger creates a logger that can filter non-critical messages
func NewFilteredLogger(file *os.File, verbose *bool, vMu *sync.RWMutex) *FilteredLogger {
func NewFilteredLogger(writer io.Writer, verbose *bool, vMu *sync.RWMutex) *FilteredLogger {
return &FilteredLogger{
file: file,
writer: writer,
verbose: verbose,
vMu: vMu,
}
Expand All @@ -56,7 +56,7 @@

l.mu.Lock()
defer l.mu.Unlock()
return l.file.Write(p)
return l.writer.Write(p)
}

// Manager handles log file lifecycle and configuration
Expand All @@ -77,13 +77,11 @@
mgr.FilePath = filepath.Join(logDir, serviceName+".log")

// Try to create log directory
//nolint:gosec
if err := os.MkdirAll(logDir, 0750); err != nil {

Check failure on line 80 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G703: Path traversal via taint analysis (gosec)
// Permission denied - fallback to stdout (console mode)
log.SetOutput(os.Stdout)
mgr.FilePath = ""
//nolint:gosec
log.Printf("[i] Logging to stdout (no write access to %q)", logDir)

Check failure on line 84 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
return mgr, nil
}

Expand All @@ -93,17 +91,21 @@
}

// Open log file
f, err := os.OpenFile(mgr.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
securePath, err := secureFilepath(filepath.Dir(mgr.FilePath), mgr.FilePath)
if err != nil {
return mgr, err
}
f, err := os.OpenFile(securePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)

Check failure on line 98 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G304: Potential file inclusion via variable (gosec)
if err != nil {
// Fallback to stdout
log.SetOutput(os.Stdout)
log.Printf("[i] Logging to stdout (cannot open %s: %v)", mgr.FilePath, err)
log.Printf("[i] Logging to stdout (cannot open %s: %v)", filepath.Clean(mgr.FilePath), err)

Check failure on line 102 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
return mgr, nil
}

mgr.file = f
log.SetOutput(NewFilteredLogger(f, &mgr.Verbose, &mgr.mu))
log.Printf("[i] Logging to: %s", mgr.FilePath)
log.Printf("[i] Logging to: %s", filepath.Clean(mgr.FilePath))

Check failure on line 108 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)

return mgr, nil
}
Expand Down Expand Up @@ -151,10 +153,14 @@
}

// Reopen file
f, err := os.OpenFile(m.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
securePath, err := secureFilepath(filepath.Dir(m.FilePath), m.FilePath)
if err != nil {
return err
}
f, err := os.OpenFile(securePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)

Check failure on line 160 in internal/logging/logging.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G304: Potential file inclusion via variable (gosec)
if err != nil {
log.SetOutput(os.Stdout)
log.Printf("[i] Logging to stdout (cannot open %s: %v)", m.FilePath, err)
log.Printf("[i] Logging to stdout (cannot open %s: %v)", filepath.Clean(m.FilePath), err)
return err
}

Expand Down
73 changes: 73 additions & 0 deletions internal/logging/logging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package logging

import (
"bytes"
"strings"
"sync"
"testing"
)

func TestFilteredLogger_Write(t *testing.T) {
tests := []struct {
name string
verbose bool
message string
expectOutput bool
}{
{
name: "discard non-critical when not verbose",
verbose: false,
message: "[~] Modo prueba activado: test mode",
expectOutput: false,
},
{
name: "keep critical when not verbose",
verbose: false,
message: "CRITICAL: System crash",
expectOutput: true,
},
{
name: "keep non-critical when verbose",
verbose: true,
message: "[~] Modo prueba activado: test mode",
expectOutput: true,
},
{
name: "discard another non-critical when not verbose",
verbose: false,
message: "2023/10/26 12:00:00 [>] Peso enviado a cliente",
expectOutput: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
vMu := &sync.RWMutex{}
verbose := tc.verbose

logger := NewFilteredLogger(&buf, &verbose, vMu)

n, err := logger.Write([]byte(tc.message))

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if n != len(tc.message) {
t.Errorf("expected %d bytes written, got %d", len(tc.message), n)
}

output := buf.String()
hasOutput := len(output) > 0

if hasOutput != tc.expectOutput {
t.Errorf("expected output: %v, got output: %v (output: %q)", tc.expectOutput, hasOutput, output)
}

if tc.expectOutput && !strings.Contains(output, tc.message) {
t.Errorf("expected output to contain %q, but got %q", tc.message, output)
}
})
}
}
10 changes: 7 additions & 3 deletions internal/logging/rotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
// RotateIfNeeded truncates the log file if it exceeds MaxLogSize
// Keeps the last 1000 lines for continuity
func RotateIfNeeded(path string) error {
info, err := os.Stat(path)
securePath, err := secureFilepath(filepath.Dir(path), path)
if err != nil {
return err
}
info, err := os.Stat(securePath)
if err != nil {
if os.IsNotExist(err) {
return nil
Expand All @@ -32,7 +36,7 @@
}

content := strings.Join(lines, "\n") + "\n"
return os.WriteFile(path, []byte(content), 0600)
return os.WriteFile(securePath, []byte(content), 0600)
}

// ReadLastNLines reads the last n lines from a file efficiently
Expand All @@ -43,7 +47,7 @@
if err != nil {
return []string{}
}
file, err := os.Open(securePath) //nolint:gosec
file, err := os.Open(securePath)

Check failure on line 50 in internal/logging/rotation.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G304: Potential file inclusion via variable (gosec)
if err != nil {
return []string{}
}
Expand Down
1 change: 0 additions & 1 deletion internal/server/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ type ConfigMessage struct {
Marca string `json:"marca"`
ModoPrueba bool `json:"modoPrueba"`
Dir string `json:"dir,omitempty"`
//nolint:gosec
AuthToken string `json:"auth_token"` // Required for config changes
}

Expand Down
4 changes: 0 additions & 4 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@

w.Header().Set("Content-Type", "text/html; charset=utf-8")
data := struct {
//nolint:gosec
AuthToken string
}{
AuthToken: config.AuthToken,
Expand All @@ -192,8 +191,7 @@

// Check lockout FIRST
if s.auth.IsLockedOut(ip) {
//nolint:gosec
log.Printf("[AUDIT] LOGIN_BLOCKED | IP=%q | reason=lockout", ip)

Check failure on line 194 in internal/server/server.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
http.Redirect(w, r, "/login?locked=1", http.StatusSeeOther)
return
}
Expand All @@ -201,8 +199,7 @@
password := r.FormValue("password")
if !s.auth.ValidatePassword(password) {
s.auth.RecordFailedLogin(ip)
//nolint:gosec
log.Printf("[AUDIT] LOGIN_FAILED | IP=%q", ip)

Check failure on line 202 in internal/server/server.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
http.Redirect(w, r, "/login?error=1", http.StatusSeeOther)
return
}
Expand All @@ -210,8 +207,7 @@
// Success
s.auth.ClearFailedLogins(ip)
s.auth.SetSessionCookie(w)
//nolint:gosec
log.Printf("[AUDIT] LOGIN_SUCCESS | IP=%q", ip)

Check failure on line 210 in internal/server/server.go

View workflow job for this annotation

GitHub Actions / πŸ” Linting

G706: Log injection via taint analysis (gosec)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

Expand Down
5 changes: 5 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1. **Fix `gosec` lint issues**: Reapply fixes for path traversal vulnerabilities (G703) in the `logging` package by validating paths using `secureFilepath` in `RotateIfNeeded` and other file operations. Resolve log injection warnings (G706) by sanitizing inputs using `filepath.Clean` before printing in `log.Printf`. Fix G124 by adding missing Secure and SameSite attributes to HTTP cookies in the auth package. The earlier attempt introduced errors (like `path` instead of `securePath` and syntax errors in `SameSite`). This time I will be careful to get it right.
2. **Review semantic PR title**: The PR title requires a correct release type. Ensure the commit uses the `fix` release type as there's a fix for gosec findings in the PR. E.g. `fix(security): resolve gosec findings in auth, logging, and server packages`.
3. **Run tests & linter locally**: Verify the changes by running tests and linter locally.
4. **Pre-commit**: Complete pre commit steps to make sure proper testing, verifications, reviews and reflections are done.
5. **Submit**: Create PR documenting the testing improvement and security fixes.
Loading