Skip to content

Commit

Permalink
fix(x-pack/filebeat/input/http_endpoint): hmac header validation (#42756
Browse files Browse the repository at this point in the history
) (#42759)

The HMAC signature validation code had an optimization intended to
return early when the configured HMAC header was not present in the
request. However, it was checking the wrong variable for emptiness,
which effectively skipped this check. If a request included an empty
HMAC header, the HMAC signature check would still proceed and fail due
to the missing or incorrect signature.

This issue has been corrected by this commit. The code now returns
`errMissingHMACHeader` only when the header is truly absent (not present
rather than having an empty value). Additionally, before decoding the
signature, a check for an empty value is added to return a descriptive error.

(cherry picked from commit a74865b)

Co-authored-by: Andrew Kroh <[email protected]>
  • Loading branch information
mergify[bot] and andrewkroh authored Feb 19, 2025
1 parent 097041c commit 4d00574
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
- Fix a bug in Salesforce input to only handle responses with 200 status code {pull}41015[41015]
- [Journald] Fixes handling of `journalctl` restart. A known symptom was broken multiline messages when there was a restart of journalctl while aggregating the lines. {issue}41331[41331] {pull}42595[42595]
- Fix entityanalytics activedirectory provider full sync use before initialization bug. {pull}42682[42682]
- In the `http_endpoint` input, fix the check for a missing HMAC HTTP header. {pull}42756[42756]

*Heartbeat*

Expand Down
75 changes: 75 additions & 0 deletions x-pack/filebeat/input/http_endpoint/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,81 @@ func Test_apiResponse(t *testing.T) {
wantStatus: http.StatusOK,
wantResponse: `{"message": "success"}`,
},
{
name: "hmac_header_not_present",
conf: func() config {
c := defaultConfig()
c.HMACHeader = "Authorization"
c.HMACKey = "mysecretkey"
c.HMACType = "sha256"
c.HMACPrefix = "HMAC-SHA256 "
return c
}(),
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`))
req.Header.Set("Content-Type", "application/json")
return req
}(),
wantStatus: http.StatusUnauthorized,
wantResponse: `{"message":"missing HMAC header"}`,
},
{
name: "hmac_header_value_is_empty",
conf: func() config {
c := defaultConfig()
c.HMACHeader = "Authorization"
c.HMACKey = "mysecretkey"
c.HMACType = "sha256"
c.HMACPrefix = "HMAC-SHA256 "
return c
}(),
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "")
return req
}(),
wantStatus: http.StatusUnauthorized,
wantResponse: `{"message":"invalid HMAC signature encoding: unexpected empty header value"}`,
},
{
name: "hmac_header_value_only_contains_prefix",
conf: func() config {
c := defaultConfig()
c.HMACHeader = "Authorization"
c.HMACKey = "mysecretkey"
c.HMACType = "sha256"
c.HMACPrefix = "HMAC-SHA256 "
return c
}(),
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "HMAC-SHA256 ")
return req
}(),
wantStatus: http.StatusUnauthorized,
wantResponse: `{"message":"invalid HMAC signature encoding: unexpected empty header value"}`,
},
{
name: "hmac_header_value_bad_encoding",
conf: func() config {
c := defaultConfig()
c.HMACHeader = "Authorization"
c.HMACKey = "mysecretkey"
c.HMACType = "sha256"
c.HMACPrefix = "HMAC-SHA256 "
return c
}(),
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"id":0}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "HMAC-SHA256 not-hex-or-base64")
return req
}(),
wantStatus: http.StatusUnauthorized,
wantResponse: `{"message":"invalid HMAC signature encoding: encoding/hex: invalid byte: U+006E 'n'\nillegal base64 data at input byte 3\nillegal base64 data at input byte 3"}`,
},
{
name: "single_event_gzip",
conf: defaultConfig(),
Expand Down
21 changes: 13 additions & 8 deletions x-pack/filebeat/input/http_endpoint/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,15 @@ func (v *apiValidator) validateRequest(r *http.Request) (status int, err error)
}

if v.hmacHeader != "" && v.hmacKey != "" && v.hmacType != "" {
// Read HMAC signature from HTTP header.
hmacHeaderValue := r.Header.Get(v.hmacHeader)
if v.hmacHeader == "" {
// Check whether the HMAC header exists at all.
if len(r.Header.Values(v.hmacHeader)) == 0 {
return http.StatusUnauthorized, errMissingHMACHeader
}
if v.hmacPrefix != "" {
hmacHeaderValue = strings.TrimPrefix(hmacHeaderValue, v.hmacPrefix)
}
signature, err := decodeHeaderValue(hmacHeaderValue)
// Read HMAC signature from HTTP header.
hmacHeaderValue := r.Header.Get(v.hmacHeader)
signature, err := decodeHeaderValue(strings.TrimPrefix(hmacHeaderValue, v.hmacPrefix))
if err != nil {
return http.StatusUnauthorized, fmt.Errorf("invalid HMAC signature hex: %w", err)
return http.StatusUnauthorized, fmt.Errorf("invalid HMAC signature encoding: %w", err)
}

// We need access to the request body to validate the signature, but we
Expand Down Expand Up @@ -113,7 +111,14 @@ var decoders = [...]func(string) ([]byte, error){
base64.StdEncoding.DecodeString,
}

// decodeHeaderValue attempts to decode s as hex, unpadded base64
// ([base64.RawStdEncoding]), and padded base64 ([base64.StdEncoding]).
// The first successful decoding result is returned. If all decodings fail, it
// collects errors from each attempt and returns them as a single error.
func decodeHeaderValue(s string) ([]byte, error) {
if s == "" {
return nil, errors.New("unexpected empty header value")
}
var errs []error
for _, d := range &decoders {
b, err := d(s)
Expand Down

0 comments on commit 4d00574

Please sign in to comment.