Skip to content

Commit 338547d

Browse files
authored
Add request body validation (#436)
1 parent f32e9cb commit 338547d

File tree

4 files changed

+63
-42
lines changed

4 files changed

+63
-42
lines changed

tests/unit/test_request_validator.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,57 @@
1010
class ValidationTest(unittest.TestCase):
1111

1212
def setUp(self):
13-
token = "1c892n40nd03kdnc0112slzkl3091j20"
13+
token = "12345"
1414
self.validator = RequestValidator(token)
1515

16-
self.uri = "http://www.postbin.org/1ed898x"
16+
self.uri = "https://mycompany.com/myapp.php?foo=1&bar=2"
1717
self.params = {
18-
"AccountSid": "AC9a9f9392lad99kla0sklakjs90j092j3",
19-
"ApiVersion": "2010-04-01",
20-
"CallSid": "CAd800bb12c0426a7ea4230e492fef2a4f",
21-
"CallStatus": "ringing",
22-
"Called": "+15306384866",
23-
"CalledCity": "OAKLAND",
24-
"CalledCountry": "US",
25-
"CalledState": "CA",
26-
"CalledZip": "94612",
27-
"Caller": "+15306666666",
28-
"CallerCity": "SOUTH LAKE TAHOE",
29-
"CallerCountry": "US",
30-
"CallerName": "CA Wireless Call",
31-
"CallerState": "CA",
32-
"CallerZip": "89449",
33-
"Direction": "inbound",
34-
"From": "+15306666666",
35-
"FromCity": "SOUTH LAKE TAHOE",
36-
"FromCountry": "US",
37-
"FromState": "CA",
38-
"FromZip": "89449",
39-
"To": "+15306384866",
40-
"ToCity": "OAKLAND",
41-
"ToCountry": "US",
42-
"ToState": "CA",
43-
"ToZip": "94612",
18+
"CallSid": "CA1234567890ABCDE",
19+
"Digits": "1234",
20+
"From": "+14158675309",
21+
"To": "+18005551212",
22+
"Caller": "+14158675309",
4423
}
24+
self.expected = "RSOYDt4T1cUTdK1PDd93/VVr8B8="
25+
self.body = "{\"property\": \"value\", \"boolean\": true}"
26+
self.bodyHash = "Ch/3Y02as7ldtcmi3+lBbkFQKyg6gMfPGWMmMvluZiA="
27+
self.encodedBodyHash = self.bodyHash.replace("+", "%2B").replace("=", "%3D")
28+
self.uriWithBody = self.uri + "&bodySHA256=" + self.encodedBodyHash
4529

4630
def test_compute_signature_bytecode(self):
47-
expected = b("fF+xx6dTinOaCdZ0aIeNkHr/ZAA=")
31+
expected = b(self.expected)
4832
signature = self.validator.compute_signature(self.uri,
4933
self.params,
5034
utf=False)
5135
assert_equal(signature, expected)
5236

5337
def test_compute_signature_unicode(self):
54-
expected = u("fF+xx6dTinOaCdZ0aIeNkHr/ZAA=")
38+
expected = u(self.expected)
5539
signature = self.validator.compute_signature(self.uri,
5640
self.params,
5741
utf=True)
5842
assert_equal(signature, expected)
5943

44+
def test_compute_hash_bytecode(self):
45+
expected = b(self.bodyHash)
46+
body_hash = self.validator.compute_hash(self.body, utf=False)
47+
48+
assert_equal(expected, body_hash)
49+
50+
def test_compute_hash_unicode(self):
51+
expected = u(self.bodyHash)
52+
body_hash = self.validator.compute_hash(self.body, utf=True)
53+
54+
assert_equal(expected, body_hash)
55+
6056
def test_validation(self):
61-
expected = "fF+xx6dTinOaCdZ0aIeNkHr/ZAA="
62-
assert_true(self.validator.validate(self.uri, self.params, expected))
57+
assert_true(self.validator.validate(self.uri, self.params, self.expected))
6358

6459
def test_validation_removes_port_on_https(self):
65-
self.uri = "https://www.postbin.org:1234/1ed898x"
66-
expected = "Y7MeICc5ECftd1G11Fc8qoxAn0A="
67-
assert_true(self.validator.validate(self.uri, self.params, expected))
60+
uri = self.uri.replace(".com", ".com:1234")
61+
assert_true(self.validator.validate(uri, self.params, self.expected))
62+
63+
def test_validation_of_body_succeeds(self):
64+
uri = self.uriWithBody
65+
is_valid = self.validator.validate(uri, self.body, "afcFvPLPYT8mg/JyIVkdnqQKa2s=")
66+
assert_true(is_valid)

twilio/.request_validator.py.swp

12 KB
Binary file not shown.

twilio/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Those are not supported by the six library and needs to be done manually
22
try:
33
# python 3
4-
from urllib.parse import urlencode, urlparse, urljoin, urlunparse
4+
from urllib.parse import urlencode, urlparse, urljoin, urlunparse, parse_qs
55
except ImportError:
66
# python 2 backward compatibility
77
# noinspection PyUnresolvedReferences
88
from urllib import urlencode
99
# noinspection PyUnresolvedReferences
10-
from urlparse import urlparse, urljoin, urlunparse
10+
from urlparse import urlparse, urljoin, urlunparse, parse_qs
1111

1212
try:
1313
# python 2

twilio/request_validator.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import base64
22
import hmac
3-
from hashlib import sha1
3+
from hashlib import sha1, sha256
44

5-
from six import PY3
5+
from six import PY3, string_types
66

7-
from twilio.compat import izip, urlparse
7+
from twilio.compat import izip, urlparse, parse_qs
88

99

1010
def compare(string1, string2):
@@ -64,16 +64,38 @@ def compute_signature(self, uri, params, utf=PY3):
6464

6565
return computed.strip()
6666

67+
def compute_hash(self, body, utf=PY3):
68+
computed = base64.b64encode(sha256(body.encode("utf-8")).digest())
69+
70+
if utf:
71+
computed = computed.decode('utf-8')
72+
73+
return computed.strip()
74+
6775
def validate(self, uri, params, signature):
6876
"""Validate a request from Twilio
6977
7078
:param uri: full URI that Twilio requested on your server
71-
:param params: post vars that Twilio sent with the request
79+
:param params: dictionary of POST variables or string of POST body for JSON requests
7280
:param signature: expected signature in HTTP X-Twilio-Signature header
7381
7482
:returns: True if the request passes validation, False if not
7583
"""
84+
if params is None:
85+
params = {}
86+
7687
parsed_uri = urlparse(uri)
7788
if parsed_uri.scheme == "https" and parsed_uri.port:
7889
uri = remove_port(parsed_uri)
79-
return compare(self.compute_signature(uri, params), signature)
90+
91+
valid_signature = False # Default fail
92+
valid_body_hash = True # May not receive body hash, so default succeed
93+
94+
query = parse_qs(parsed_uri.query)
95+
if "bodySHA256" in query and isinstance(params, string_types):
96+
valid_body_hash = compare(self.compute_hash(params), query["bodySHA256"][0])
97+
valid_signature = compare(self.compute_signature(uri, {}), signature)
98+
else:
99+
valid_signature = compare(self.compute_signature(uri, params), signature)
100+
101+
return valid_signature and valid_body_hash

0 commit comments

Comments
 (0)