Honor RoundTripper contract in middleware; validate incoming X-Request-Id
BearerAuth, BasicAuth and DefaultHeaders mutated the caller's request, which violates the RoundTripper contract and risks races on shared/retried requests; clone before writing headers (matching RequestID). Validate the incoming X-Request-Id (length and character set) before propagating it to logs and the response header, preventing log forging and header splitting from a client-controlled value.
This commit is contained in:
@@ -9,9 +9,14 @@ import (
|
||||
"git.codelab.vc/pkg/httpx/internal/requestid"
|
||||
)
|
||||
|
||||
// maxRequestIDLen bounds the length of a client-supplied request ID that we
|
||||
// are willing to propagate.
|
||||
const maxRequestIDLen = 128
|
||||
|
||||
// RequestID returns a middleware that assigns a unique request ID to each
|
||||
// request. If the incoming request already has an X-Request-Id header, that
|
||||
// value is used. Otherwise a new UUID v4 is generated via crypto/rand.
|
||||
// request. If the incoming request carries a valid X-Request-Id header, that
|
||||
// value is reused; otherwise (or if the supplied value is empty, too long, or
|
||||
// contains unsafe characters) a new UUID v4 is generated via crypto/rand.
|
||||
//
|
||||
// The request ID is stored in the request context (retrieve with
|
||||
// RequestIDFromContext) and set on the response X-Request-Id header.
|
||||
@@ -19,7 +24,7 @@ func RequestID() Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.Header.Get("X-Request-Id")
|
||||
if id == "" {
|
||||
if !validRequestID(id) {
|
||||
id = newUUID()
|
||||
}
|
||||
|
||||
@@ -30,6 +35,27 @@ func RequestID() Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
// validRequestID reports whether a client-supplied request ID is safe to
|
||||
// propagate: non-empty, within a sane length, and restricted to characters
|
||||
// that cannot forge log lines or split response headers.
|
||||
func validRequestID(id string) bool {
|
||||
if id == "" || len(id) > maxRequestIDLen {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(id); i++ {
|
||||
c := id[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z',
|
||||
c >= 'A' && c <= 'Z',
|
||||
c >= '0' && c <= '9',
|
||||
c == '-', c == '_', c == '.':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RequestIDFromContext returns the request ID from the context, or an empty
|
||||
// string if none is set.
|
||||
func RequestIDFromContext(ctx context.Context) string {
|
||||
|
||||
Reference in New Issue
Block a user