fix(dms): apierror.Normalize verbirgt 5xx-Texte (QA-15 F-14) #375

Closed
patrick wants to merge 1 commits from fix/dms-httpharden-normalize into main
pull from: fix/dms-httpharden-normalize
3 changed files with 74 additions and 8 deletions

No files matched your search

+34 -7
View File
@@ -3,7 +3,10 @@ package apierror
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"strings"
)
@@ -45,8 +48,9 @@ func CodeForStatus(status int) string {
type body struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Code string `json:"code"`
Message string `json:"message"`
CorrelationID string `json:"correlation_id,omitempty"`
} `json:"error"`
}
@@ -59,13 +63,22 @@ func Write(w http.ResponseWriter, status int, code, message string) {
_ = json.NewEncoder(w).Encode(b)
}
func newCorrelationID() string {
buf := make([]byte, 8)
if _, err := rand.Read(buf); err != nil {
return "korrelations-id-nicht-verfuegbar"
}
return hex.EncodeToString(buf)
}
// Normalize schreibt jede Fehlerantwort (Status >= 400) im Core-Format um;
// Erfolgsantworten laufen ungepuffert durch.
// Erfolgsantworten laufen ungepuffert durch. 5xx-Texte werden nie an den
// Aufrufer gegeben (Interna-Leck, QA-15 F-14), nur mit Korrelations-ID geloggt.
func Normalize(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &recorder{ResponseWriter: w}
next.ServeHTTP(rec, r)
rec.finish()
rec.finish(r)
})
}
@@ -106,13 +119,27 @@ func (r *recorder) Flush() {
func (r *recorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
func (r *recorder) finish() {
func (r *recorder) finish(req *http.Request) {
if !r.buffered {
return
}
out := map[string]any{}
message := strings.TrimSpace(r.buf.String())
code := CodeForStatus(r.status)
rawMessage := strings.TrimSpace(r.buf.String())
if r.status >= 500 {
corrID := newCorrelationID()
log.Printf("apierror: 5xx %s %s status=%d korrelations-id=%s ursprung=%q", req.Method, req.URL.Path, r.status, corrID, rawMessage)
out := body{}
out.Error.Code = code
out.Error.Message = "Interner Fehler"
out.Error.CorrelationID = corrID
encoded, _ := json.Marshal(out)
r.flush(append(encoded, '\n'))
return
}
out := map[string]any{}
message := rawMessage
var parsed map[string]any
if json.Unmarshal(r.buf.Bytes(), &parsed) == nil && parsed != nil {
+1 -1
View File
@@ -43,7 +43,7 @@ func TestNormalizeRewritesLegacyErrorShapes(t *testing.T) {
_, _ = w.Write([]byte(`{"error":"ungueltiger wert"}`))
}, 422, "unprocessable", "ungueltiger wert"},
{"leerer body", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(403) }, 403, "forbidden", "Forbidden"},
{"5xx", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", 500) }, 500, "internal_error", "boom"},
{"5xx", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", 500) }, 500, "internal_error", "Interner Fehler"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
+39
View File
@@ -0,0 +1,39 @@
package apierror
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestNormalize5xxHidesInternalsAndSetsCorrelationID(t *testing.T) {
const secret = "pq: connection refused to db-internal:5432 /var/lib/dms/secret.key"
rec := httptest.NewRecorder()
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, secret, http.StatusInternalServerError)
})).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
if strings.Contains(rec.Body.String(), "db-internal") || strings.Contains(rec.Body.String(), "secret.key") {
t.Fatalf("5xx-Interna im Body: %q", rec.Body.String())
}
e, _ := decode(t, rec)["error"].(map[string]any)
if id, _ := e["correlation_id"].(string); id == "" {
t.Errorf("correlation_id fehlt: %v", e)
}
}
func TestNormalize4xxKeepsMessageWithoutCorrelationID(t *testing.T) {
rec := httptest.NewRecorder()
Normalize(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "dokument nicht gefunden", http.StatusNotFound)
})).ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil))
e, _ := decode(t, rec)["error"].(map[string]any)
if _, has := e["correlation_id"]; has {
t.Errorf("4xx darf keine correlation_id tragen: %v", e)
}
if msg, _ := e["message"].(string); msg != "dokument nicht gefunden" {
t.Errorf("message = %q", msg)
}
}