diff --git a/deploy/test/loadtest/docker-compose.yml b/deploy/test/loadtest/docker-compose.yml index f5102cc..0d5d525 100644 --- a/deploy/test/loadtest/docker-compose.yml +++ b/deploy/test/loadtest/docker-compose.yml @@ -290,7 +290,15 @@ services: # /healthz endpoint. # --------------------------------------------------------------------------- f5-mock-target: - build: ../f5-mock-icontrol + # Long-form build to match docker-compose.test.yml: the Dockerfile + # has `COPY deploy/test/f5-mock-icontrol/ ./` which assumes the + # build context is the REPO ROOT. The previous shorthand form + # `build: ../f5-mock-icontrol` set the context to the + # f5-mock-icontrol directory itself, breaking the COPY at CI build + # time (run #25305811340: "deploy/test/f5-mock-icontrol: not found"). + build: + context: ../../.. + dockerfile: deploy/test/f5-mock-icontrol/Dockerfile container_name: certctl-loadtest-f5-mock healthcheck: test: ["CMD-SHELL", "wget -q -O- http://localhost:8080/healthz || exit 1"] diff --git a/internal/api/middleware/middleware.go b/internal/api/middleware/middleware.go index 4e795ee..1c7ca6c 100644 --- a/internal/api/middleware/middleware.go +++ b/internal/api/middleware/middleware.go @@ -50,7 +50,14 @@ func RequestID(next http.Handler) http.Handler { // percent-decoded path segments in r.URL.Path, which can include CR/LF // in the decoded form even though the raw HTTP request line cannot). // strings.ReplaceAll on CR/LF/NUL strips the forgery vector before the -// log line is emitted. Closes CodeQL #17 (go/log-injection). +// log line is emitted. Closes CodeQL #17 + #32 (go/log-injection). +// +// The replacement is intentionally inlined at the call site (literal +// strings.ReplaceAll chains) because CodeQL's go/log-injection +// taint tracker only recognizes that exact pattern as a sanitizer — +// strings.NewReplacer / wrapper helpers don't trigger the recognition, +// reopening the alert. The OWASP example in the CodeQL rule docs uses +// the same pattern. func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() @@ -62,29 +69,20 @@ func Logging(next http.Handler) http.Handler { duration := time.Since(start) requestID := getRequestID(r.Context()) - log.Printf("[%s] %s %s %d %v", - requestID, - scrubLogValue(r.Method), - scrubLogValue(r.URL.Path), - wrapped.statusCode, - duration, - ) - }) -} -// scrubLogValue strips control characters that an attacker could use to -// forge log entries (CWE-117). The replacement is structural, not -// destructive: we collapse CR/LF/NUL to a single space rather than -// dropping them, so an operator scanning the log can still see that the -// field was present (just neutralized). Defense in depth: the -// production middleware should prefer NewLogging (slog with structured -// fields) where the logger handles escape on its own. -func scrubLogValue(v string) string { - if !strings.ContainsAny(v, "\r\n\x00") { - return v - } - r := strings.NewReplacer("\r", " ", "\n", " ", "\x00", " ") - return r.Replace(v) + // Strip CR/LF/NUL from attacker-controllable request fields + // before logging. Inlined per CodeQL #32 — the ReplaceAll + // chain is the pattern the analyzer pattern-matches as a + // sanitizer. + method := strings.ReplaceAll(r.Method, "\n", "") + method = strings.ReplaceAll(method, "\r", "") + method = strings.ReplaceAll(method, "\x00", "") + urlPath := strings.ReplaceAll(r.URL.Path, "\n", "") + urlPath = strings.ReplaceAll(urlPath, "\r", "") + urlPath = strings.ReplaceAll(urlPath, "\x00", "") + + log.Printf("[%s] %s %s %d %v", requestID, method, urlPath, wrapped.statusCode, duration) + }) } // NewLogging creates a structured logging middleware using slog.