Skip to content

Commit f231c10

Browse files
Validate request body for proper base64 encoding (#22)
* Validate request body for proper base64 encoding * Fix logic * Remove mock lambda handler from test * Implement safe parsing when `body` is not base64-encoded * Enable debugging and better Lambda mocking for better test visibility * Fix logic for responses, and add more tests, and minor updates * Revise logic and clean up tests
1 parent e1c2f56 commit f231c10

9 files changed

+451
-9
lines changed

moesif_aws_lambda/middleware.py

+46-9
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
import json
1515
import os
1616
from pprint import pprint
17-
import base64
1817

1918
import random
2019
import math
20+
import binascii
21+
import re
2122

2223
try:
2324
from urllib import urlencode
@@ -169,6 +170,24 @@ def build_uri(self, event, payload_format_version_1_0):
169170
uri = uri + '?' + event['rawQueryString']
170171
return uri
171172

173+
def is_base64_str(self, data):
174+
"""Checks if `data` is a valid base64-encoded string."""
175+
if not isinstance(data, str):
176+
return False
177+
if len(data) % 4 != 0:
178+
return False
179+
180+
b64_regex = re.compile("^[A-Za-z0-9+/]+={0,2}$")
181+
182+
if (not b64_regex.fullmatch(data)):
183+
return False
184+
185+
try:
186+
_ = base64.b64decode(data)
187+
return True
188+
except binascii.Error:
189+
return False
190+
172191
def base64_body(cls, data):
173192
"""Function to transfer body into base64 encoded"""
174193
body = base64.b64encode(str(data).encode("utf-8"))
@@ -178,6 +197,26 @@ def base64_body(cls, data):
178197
return str(body, "utf-8"), 'base64'
179198
else:
180199
return str(body), 'base64'
200+
201+
def safe_json_parse(self, body):
202+
"""Tries to parse the `body` as JSON safely.
203+
Returns the formatted body and the appropriate `transfer_encoding`.
204+
"""
205+
try:
206+
if isinstance(body, (dict, list)):
207+
# If body is an instance of either a dictionary of list,
208+
# we can return it as is.
209+
return body, "json"
210+
elif isinstance(body, bytes):
211+
body_str = body.decode()
212+
parsed_body = json.loads(body_str)
213+
return parsed_body, "json"
214+
else:
215+
parsed_body = json.loads(body)
216+
return parsed_body, "json"
217+
218+
except (json.JSONDecodeError, TypeError, ValueError, UnicodeError) as error:
219+
return self.base64_body(body)
181220

182221
def process_body(self, body_wrapper):
183222
"""Function to process body"""
@@ -193,18 +232,16 @@ def process_body(self, body_wrapper):
193232

194233
body = None
195234
transfer_encoding = None
235+
196236
try:
197-
if body_wrapper.get('isBase64Encoded', False):
198-
body = body_wrapper.get('body')
199-
transfer_encoding = 'base64'
200-
else:
201-
if isinstance(body_wrapper['body'], str):
202-
body = json.loads(body_wrapper.get('body'))
203-
else:
237+
if body_wrapper.get('isBase64Encoded', False) and self.is_base64_str(body_wrapper.get('body')):
204238
body = body_wrapper.get('body')
205-
transfer_encoding = 'json'
239+
transfer_encoding = 'base64'
240+
else:
241+
body, transfer_encoding = self.safe_json_parse(body_wrapper.get('body'))
206242
except Exception as e:
207243
return self.base64_body(body_wrapper['body'])
244+
208245
return body, transfer_encoding
209246

210247
def before(self, event, context):

moesif_aws_lambda/tests/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "$default",
4+
"rawPath": "/path/to/resource",
5+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
6+
"cookies": [
7+
"cookie1",
8+
"cookie2"
9+
],
10+
"headers": {
11+
"Header1": "value1",
12+
"Header2": "value1,value2"
13+
},
14+
"queryStringParameters": {
15+
"parameter1": "value1,value2",
16+
"parameter2": "value"
17+
},
18+
"requestContext": {
19+
"accountId": "123456789012",
20+
"apiId": "api-id",
21+
"authentication": {
22+
"clientCert": {
23+
"clientCertPem": "CERT_CONTENT",
24+
"subjectDN": "www.example.com",
25+
"issuerDN": "Example issuer",
26+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
27+
"validity": {
28+
"notBefore": "May 28 12:30:02 2019 GMT",
29+
"notAfter": "Aug 5 09:36:04 2021 GMT"
30+
}
31+
}
32+
},
33+
"authorizer": {
34+
"jwt": {
35+
"claims": {
36+
"claim1": "value1",
37+
"claim2": "value2"
38+
},
39+
"scopes": [
40+
"scope1",
41+
"scope2"
42+
]
43+
}
44+
},
45+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
46+
"domainPrefix": "id",
47+
"http": {
48+
"method": "POST",
49+
"path": "/path/to/resource",
50+
"protocol": "HTTP/1.1",
51+
"sourceIp": "192.168.0.1/32",
52+
"userAgent": "agent"
53+
},
54+
"requestId": "id",
55+
"routeKey": "$default",
56+
"stage": "$default"
57+
},
58+
"body": {"foo": "bar"},
59+
"pathParameters": {
60+
"parameter1": "value1"
61+
},
62+
"isBase64Encoded": true,
63+
"stageVariables": {
64+
"stageVariable1": "value1",
65+
"stageVariable2": "value2"
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "$default",
4+
"rawPath": "/path/to/resource",
5+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
6+
"cookies": [
7+
"cookie1",
8+
"cookie2"
9+
],
10+
"headers": {
11+
"Header1": "value1",
12+
"Header2": "value1,value2"
13+
},
14+
"queryStringParameters": {
15+
"parameter1": "value1,value2",
16+
"parameter2": "value"
17+
},
18+
"requestContext": {
19+
"accountId": "123456789012",
20+
"apiId": "api-id",
21+
"authentication": {
22+
"clientCert": {
23+
"clientCertPem": "CERT_CONTENT",
24+
"subjectDN": "www.example.com",
25+
"issuerDN": "Example issuer",
26+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
27+
"validity": {
28+
"notBefore": "May 28 12:30:02 2019 GMT",
29+
"notAfter": "Aug 5 09:36:04 2021 GMT"
30+
}
31+
}
32+
},
33+
"authorizer": {
34+
"jwt": {
35+
"claims": {
36+
"claim1": "value1",
37+
"claim2": "value2"
38+
},
39+
"scopes": [
40+
"scope1",
41+
"scope2"
42+
]
43+
}
44+
},
45+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
46+
"domainPrefix": "id",
47+
"http": {
48+
"method": "POST",
49+
"path": "/path/to/resource",
50+
"protocol": "HTTP/1.1",
51+
"sourceIp": "192.168.0.1/32",
52+
"userAgent": "agent"
53+
},
54+
"requestId": "id",
55+
"routeKey": "$default",
56+
"stage": "$default"
57+
},
58+
"body": 10,
59+
"pathParameters": {
60+
"parameter1": "value1"
61+
},
62+
"isBase64Encoded": true,
63+
"stageVariables": {
64+
"stageVariable1": "value1",
65+
"stageVariable2": "value2"
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "$default",
4+
"rawPath": "/path/to/resource",
5+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
6+
"cookies": [
7+
"cookie1",
8+
"cookie2"
9+
],
10+
"headers": {
11+
"Header1": "value1",
12+
"Header2": "value1,value2"
13+
},
14+
"queryStringParameters": {
15+
"parameter1": "value1,value2",
16+
"parameter2": "value"
17+
},
18+
"requestContext": {
19+
"accountId": "123456789012",
20+
"apiId": "api-id",
21+
"authentication": {
22+
"clientCert": {
23+
"clientCertPem": "CERT_CONTENT",
24+
"subjectDN": "www.example.com",
25+
"issuerDN": "Example issuer",
26+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
27+
"validity": {
28+
"notBefore": "May 28 12:30:02 2019 GMT",
29+
"notAfter": "Aug 5 09:36:04 2021 GMT"
30+
}
31+
}
32+
},
33+
"authorizer": {
34+
"jwt": {
35+
"claims": {
36+
"claim1": "value1",
37+
"claim2": "value2"
38+
},
39+
"scopes": [
40+
"scope1",
41+
"scope2"
42+
]
43+
}
44+
},
45+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
46+
"domainPrefix": "id",
47+
"http": {
48+
"method": "POST",
49+
"path": "/path/to/resource",
50+
"protocol": "HTTP/1.1",
51+
"sourceIp": "192.168.0.1/32",
52+
"userAgent": "agent"
53+
},
54+
"requestId": "id",
55+
"routeKey": "$default",
56+
"stage": "$default"
57+
},
58+
"body": "{\"foo\": \"bar\"}",
59+
"pathParameters": {
60+
"parameter1": "value1"
61+
},
62+
"isBase64Encoded": true,
63+
"stageVariables": {
64+
"stageVariable1": "value1",
65+
"stageVariable2": "value2"
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "$default",
4+
"rawPath": "/path/to/resource",
5+
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
6+
"cookies": [
7+
"cookie1",
8+
"cookie2"
9+
],
10+
"headers": {
11+
"Header1": "value1",
12+
"Header2": "value1,value2"
13+
},
14+
"queryStringParameters": {
15+
"parameter1": "value1,value2",
16+
"parameter2": "value"
17+
},
18+
"requestContext": {
19+
"accountId": "123456789012",
20+
"apiId": "api-id",
21+
"authentication": {
22+
"clientCert": {
23+
"clientCertPem": "CERT_CONTENT",
24+
"subjectDN": "www.example.com",
25+
"issuerDN": "Example issuer",
26+
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
27+
"validity": {
28+
"notBefore": "May 28 12:30:02 2019 GMT",
29+
"notAfter": "Aug 5 09:36:04 2021 GMT"
30+
}
31+
}
32+
},
33+
"authorizer": {
34+
"jwt": {
35+
"claims": {
36+
"claim1": "value1",
37+
"claim2": "value2"
38+
},
39+
"scopes": [
40+
"scope1",
41+
"scope2"
42+
]
43+
}
44+
},
45+
"domainName": "id.execute-api.us-east-1.amazonaws.com",
46+
"domainPrefix": "id",
47+
"http": {
48+
"method": "POST",
49+
"path": "/path/to/resource",
50+
"protocol": "HTTP/1.1",
51+
"sourceIp": "192.168.0.1/32",
52+
"userAgent": "agent"
53+
},
54+
"requestId": "id",
55+
"routeKey": "$default",
56+
"stage": "$default"
57+
},
58+
"body": "eyJ0ZXN0IjoiYm9keSJ9",
59+
"pathParameters": {
60+
"parameter1": "value1"
61+
},
62+
"isBase64Encoded": true,
63+
"stageVariables": {
64+
"stageVariable1": "value1",
65+
"stageVariable2": "value2"
66+
}
67+
}

moesif_aws_lambda/tests/image.png

58.9 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"statusCode": 200,
3+
"headers": {
4+
"Content-Type": "application/json"
5+
},
6+
"isBase64Encoded": false,
7+
"multiValueHeaders": {
8+
"X-Custom-Header": ["My value", "My other value"]
9+
},
10+
"body": "{ \"message\": \"Hello from Lambda!\" }"
11+
}

0 commit comments

Comments
 (0)