Skip to content

Commit 7813e2f

Browse files
authored
fix: handling of HTTP headers in EventStreams (#1396)
To include all headers you have to explicitly set the additional_data_headers = * * If the header key is used for Authentication it will be redacted * To pick and choose which headers get included, list them as a comma delimited string in additional_data_headers e.g Host,Origin,X-Request-ID * Any header that starts with X-Envoy will be dropped * X-Trusted-Proxy,X-Forwarded-For,X-Real-IP will be dropped
1 parent a8f9bcf commit 7813e2f

File tree

2 files changed

+183
-41
lines changed

2 files changed

+183
-41
lines changed

src/aap_eda/api/views/external_event_stream.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import datetime
1717
import logging
1818
import urllib.parse
19+
from typing import Any
1920

2021
import yaml
2122
from django.conf import settings
@@ -46,6 +47,8 @@
4647
from aap_eda.services.pg_notify import PGNotify
4748

4849
logger = logging.getLogger(__name__)
50+
UNSAFE_HEADER_KEYS = {"X-Trusted-Proxy", "X-Forwarded-For", "X-Real-IP"}
51+
REDACTED_STRING = "********"
4952

5053

5154
class ExternalEventStreamViewSet(viewsets.GenericViewSet):
@@ -107,20 +110,34 @@ def _parse_body(self, content_type: str, body: bytes) -> dict:
107110
raise ParseError(message) from exc
108111
return data
109112

110-
def _create_payload(
111-
self, headers: HttpHeaders, data: dict, header_key: str, endpoint: str
112-
) -> dict:
113+
def _redacted_headers(
114+
self, headers: HttpHeaders, header_key: str
115+
) -> dict[str, Any]:
113116
event_headers = {}
114117
if self.event_stream.additional_data_headers:
115-
for key in self.event_stream.additional_data_headers.split(","):
116-
value = headers.get(key)
117-
if value:
118-
event_headers[key] = value
119-
else:
120-
event_headers = dict(headers)
121-
if header_key in event_headers:
122-
event_headers.pop(header_key)
118+
if self.event_stream.additional_data_headers == "*":
119+
event_headers = dict(headers)
120+
if header_key in event_headers:
121+
event_headers[header_key] = REDACTED_STRING
122+
event_headers = {
123+
key: value
124+
for key, value in event_headers.items()
125+
if not key.startswith("X-Envoy")
126+
and key not in UNSAFE_HEADER_KEYS
127+
}
128+
else:
129+
for key in self.event_stream.additional_data_headers.split(
130+
","
131+
):
132+
key = key.strip()
133+
value = headers.get(key)
134+
if value:
135+
event_headers[key] = value
136+
return event_headers
123137

138+
def _create_payload(
139+
self, event_headers: dict, data: dict, endpoint: str
140+
) -> dict:
124141
return {
125142
"payload": data,
126143
"meta": {
@@ -226,7 +243,6 @@ def post(self, request, *_args, **kwargs):
226243
except (EventStream.DoesNotExist, ValidationError) as exc:
227244
raise ParseError("bad uuid specified") from exc
228245

229-
logger.debug("Headers %s", request.headers)
230246
logger.debug("Body %s", request.body)
231247

232248
try:
@@ -235,13 +251,17 @@ def post(self, request, *_args, **kwargs):
235251
logger.warning("Error fetching external secrets %s", str(err))
236252
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
237253

254+
event_headers = self._redacted_headers(
255+
request.headers, inputs["http_header_key"]
256+
)
257+
238258
if inputs["http_header_key"] not in request.headers:
239259
message = f"{inputs['http_header_key']} header is missing"
240260
logger.error(message)
241261
if self.event_stream.test_mode:
242262
self._update_test_data(
243263
error_message=message,
244-
headers=yaml.dump(dict(request.headers)),
264+
headers=yaml.dump(event_headers),
245265
)
246266
raise ParseError(message)
247267

@@ -260,17 +280,16 @@ def post(self, request, *_args, **kwargs):
260280
logger.debug("Data: %s", data)
261281

262282
payload = self._create_payload(
263-
request.headers,
283+
event_headers,
264284
data,
265-
inputs["http_header_key"],
266285
request.get_full_path(),
267286
)
268287
self._update_stats()
269288
if self.event_stream.test_mode:
270289
self._update_test_data(
271290
content=yaml.dump(body),
272291
content_type=request.headers.get("Content-Type", "unknown"),
273-
headers=yaml.dump(dict(request.headers)),
292+
headers=yaml.dump(event_headers),
274293
)
275294
else:
276295
try:

tests/integration/api/test_event_stream_token.py

Lines changed: 148 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
from rest_framework import status
2020
from rest_framework.test import APIClient
2121

22+
from aap_eda.api.views.external_event_stream import (
23+
REDACTED_STRING,
24+
UNSAFE_HEADER_KEYS,
25+
)
2226
from aap_eda.core import enums
2327
from tests.integration.api.test_event_stream import (
2428
create_event_stream,
@@ -80,47 +84,160 @@ def test_post_event_stream_with_token(
8084
assert response.status_code == auth_status
8185

8286

87+
BASE_HEADERS = {
88+
"X-Gitlab-Event-Uuid": "c2675c66-7e6e-4fe2-9ac3-288534ef34b9",
89+
"X-Gitlab-Instance": "https://gitlab.com",
90+
"X-Gitlab-Token": secrets.token_hex(32),
91+
"X-Gitlab-Uuid": "b697868f-3b59-4a1f-985d-47f79e2b05ff",
92+
"X-Gitlab-Event": "Push Hook",
93+
"X-Envoy-abc": "abc",
94+
"X-Trusted-Proxy": "gobbledegook",
95+
"X-Forwarded-For": "fred",
96+
"X-Real-IP": "barney",
97+
}
98+
99+
100+
@pytest.mark.parametrize(
101+
("test_args"),
102+
[
103+
(
104+
{
105+
"auth_header": "X-Gitlab-Token",
106+
"additional_data_headers": (
107+
"x-gitlab-event, x-gitlab-event-uuid , x-gitlab-uuid"
108+
),
109+
"headers": BASE_HEADERS,
110+
"required_header_keys": [
111+
"X-Gitlab-Event",
112+
"X-Gitlab-Event-Uuid",
113+
"X-Gitlab-Uuid",
114+
],
115+
"keys_should_not_exist": list(UNSAFE_HEADER_KEYS)
116+
+ [
117+
"X-Envoy-abc",
118+
"X-Gitlab-Instance",
119+
"X-Gitlab-Token",
120+
],
121+
"redacted": True,
122+
"key_remap": {
123+
"X-Gitlab-Event": "x-gitlab-event",
124+
"X-Gitlab-Event-Uuid": "x-gitlab-event-uuid",
125+
"X-Gitlab-Uuid": "x-gitlab-uuid",
126+
},
127+
"test_name": "lowercase data headers with extra spaces",
128+
}
129+
),
130+
(
131+
{
132+
"auth_header": "X-Gitlab-Token",
133+
"additional_data_headers": (
134+
"X-Gitlab-Event, X-Gitlab-Event-Uuid, X-Gitlab-Uuid"
135+
),
136+
"headers": BASE_HEADERS,
137+
"required_header_keys": [
138+
"X-Gitlab-Event",
139+
"X-Gitlab-Event-Uuid",
140+
"X-Gitlab-Uuid",
141+
],
142+
"keys_should_not_exist": list(UNSAFE_HEADER_KEYS)
143+
+ [
144+
"X-Envoy-abc",
145+
"X-Gitlab-Instance",
146+
"X-Gitlab-Token",
147+
],
148+
"redacted": True,
149+
"key_remap": {},
150+
"test_name": "data headers with extra spaces",
151+
}
152+
),
153+
(
154+
{
155+
"auth_header": "X-Gitlab-Token",
156+
"additional_data_headers": " X-Gitlab-Event ",
157+
"headers": BASE_HEADERS,
158+
"required_header_keys": ["X-Gitlab-Event"],
159+
"keys_should_not_exist": list(UNSAFE_HEADER_KEYS)
160+
+ [
161+
"X-Gitlab-Event-Uuid",
162+
"X-Gitlab-Uuid",
163+
"X-Gitlab-Token",
164+
"X-Gitlab-Instance",
165+
],
166+
"redacted": True,
167+
"key_remap": {},
168+
"test_name": "single data headers with surrounding spaces",
169+
}
170+
),
171+
(
172+
{
173+
"auth_header": "X-Gitlab-Token",
174+
"additional_data_headers": " X-Gitlab-Token ",
175+
"headers": BASE_HEADERS,
176+
"required_header_keys": ["X-Gitlab-Token"],
177+
"keys_should_not_exist": list(UNSAFE_HEADER_KEYS)
178+
+ [
179+
"X-Envoy-abc",
180+
"X-Gitlab-Event-Uuid",
181+
"X-Gitlab-Uuid",
182+
"X-Gitlab-Instance",
183+
"X-Gitlab-Event",
184+
],
185+
"redacted": False,
186+
"key_remap": {},
187+
"test_name": "single data header with exposed auth_header",
188+
}
189+
),
190+
(
191+
{
192+
"auth_header": "X-Gitlab-Token",
193+
"additional_data_headers": "*",
194+
"headers": BASE_HEADERS,
195+
"required_header_keys": [
196+
"X-Gitlab-Event",
197+
"X-Gitlab-Event-Uuid",
198+
"X-Gitlab-Instance",
199+
"X-Gitlab-Uuid",
200+
"X-Gitlab-Token",
201+
],
202+
"keys_should_not_exist": list(UNSAFE_HEADER_KEYS)
203+
+ ["X-Envoy-abc"],
204+
"redacted": True,
205+
"key_remap": {},
206+
"test_name": "wild card data header",
207+
}
208+
),
209+
],
210+
)
83211
@pytest.mark.django_db
84212
def test_post_event_stream_with_test_mode_extra_headers(
85213
admin_client: APIClient,
86214
preseed_credential_types,
215+
test_args,
87216
):
88-
secret = secrets.token_hex(32)
89-
signature_header_name = "X-Gitlab-Token"
217+
auth_header = test_args["auth_header"]
90218
inputs = {
91219
"auth_type": "token",
92-
"token": secret,
93-
"http_header_key": signature_header_name,
220+
"token": test_args["headers"][auth_header],
221+
"http_header_key": auth_header,
94222
}
95223

96224
obj = create_event_stream_credential(
97225
admin_client, enums.EventStreamCredentialType.TOKEN.value, inputs
98226
)
99227

100-
additional_data_headers = (
101-
"X-Gitlab-Event,X-Gitlab-Event-Uuid,X-Gitlab-Uuid"
102-
)
103228
data_in = {
104229
"name": "test-es-1",
105230
"eda_credential_id": obj["id"],
106231
"event_stream_type": obj["credential_type"]["kind"],
107232
"organization_id": get_default_test_org().id,
108233
"test_mode": True,
109-
"additional_data_headers": additional_data_headers,
234+
"additional_data_headers": test_args["additional_data_headers"],
110235
}
111236
event_stream = create_event_stream(admin_client, data_in)
112237
data = {"a": 1, "b": 2}
113-
headers = {
114-
"X-Gitlab-Event-Uuid": "c2675c66-7e6e-4fe2-9ac3-288534ef34b9",
115-
"X-Gitlab-Instance": "https://gitlab.com",
116-
signature_header_name: secret,
117-
"X-Gitlab-Uuid": "b697868f-3b59-4a1f-985d-47f79e2b05ff",
118-
"X-Gitlab-Event": "Push Hook",
119-
}
120-
121238
response = admin_client.post(
122239
event_stream_post_url(event_stream.uuid),
123-
headers=headers,
240+
headers=test_args["headers"],
124241
data=data,
125242
)
126243
assert response.status_code == status.HTTP_200_OK
@@ -129,15 +246,21 @@ def test_post_event_stream_with_test_mode_extra_headers(
129246
test_data = yaml.safe_load(event_stream.test_content)
130247
assert test_data["a"] == 1
131248
assert test_data["b"] == 2
249+
132250
test_headers = yaml.safe_load(event_stream.test_headers)
133-
assert (
134-
test_headers["X-Gitlab-Event-Uuid"]
135-
== "c2675c66-7e6e-4fe2-9ac3-288534ef34b9"
136-
)
137-
assert (
138-
test_headers["X-Gitlab-Uuid"] == "b697868f-3b59-4a1f-985d-47f79e2b05ff"
139-
)
140-
assert test_headers["X-Gitlab-Event"] == "Push Hook"
251+
252+
for key in test_args["required_header_keys"]:
253+
if key == auth_header and test_args["redacted"]:
254+
assert test_headers[key] == REDACTED_STRING
255+
else:
256+
assert (
257+
test_headers[test_args["key_remap"].get(key, key)]
258+
== test_args["headers"][key]
259+
)
260+
261+
for key in test_args["keys_should_not_exist"]:
262+
assert key not in test_headers
263+
141264
assert event_stream.test_content_type == "application/json"
142265
assert event_stream.events_received == 1
143266
assert event_stream.last_event_received_at is not None

0 commit comments

Comments
 (0)