Skip to content

Commit 8f212f7

Browse files
Fix API parameters signature
1 parent 30e48e7 commit 8f212f7

File tree

2 files changed

+51
-5
lines changed

2 files changed

+51
-5
lines changed

cloudinary/utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,14 +628,25 @@ def sign_request(params, options):
628628

629629

630630
def api_sign_request(params_to_sign, api_secret, algorithm=SIGNATURE_SHA1):
631-
params = [(k + "=" + (
631+
params = [_encode_param(k + "=" + (
632632
",".join(v) if isinstance(v, list) else
633633
str(v).lower() if isinstance(v, bool) else
634634
str(v)
635635
)) for k, v in params_to_sign.items() if v]
636636
to_sign = "&".join(sorted(params))
637637
return compute_hex_hash(to_sign + api_secret, algorithm)
638638

639+
def _encode_param(value):
640+
"""
641+
Encodes a parameter for safe inclusion in URL query strings.
642+
643+
Specifically replaces "&" characters with their percent-encoded equivalent "%26"
644+
to prevent them from being interpreted as parameter separators in URL query strings.
645+
646+
:param value: The parameter to encode
647+
:return: Encoded parameter
648+
"""
649+
return str(value).replace("&", "%26")
639650

640651
def breakpoint_settings_mapper(breakpoint_settings):
641652
breakpoint_settings = copy.deepcopy(breakpoint_settings)

test/test_utils.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555

5656
MOCKED_NOW = 1549533574
5757
API_SECRET = 'X7qLTrsES31MzxxkxPPA-pAGGfU'
58+
API_SIGN_REQUEST_TEST_SECRET = "hdcixPpR2iKERPwqvH6sHdK9cyac"
59+
API_SIGN_REQUEST_CLOUD_NAME = "dn6ot3ged"
5860

5961

6062
class TestUtils(unittest.TestCase):
@@ -1443,17 +1445,50 @@ def test_support_long_url_signature(self):
14431445
expected_url=DEFAULT_UPLOAD_PATH + long_signature + "/" + image_name)
14441446

14451447
def test_api_sign_request_sha1(self):
1446-
params = dict(cloud_name="dn6ot3ged", timestamp=1568810420, username="[email protected]")
1447-
signature = api_sign_request(params, "hdcixPpR2iKERPwqvH6sHdK9cyac")
1448+
params = dict(cloud_name=API_SIGN_REQUEST_CLOUD_NAME, timestamp=1568810420, username="[email protected]")
1449+
signature = api_sign_request(params, API_SIGN_REQUEST_TEST_SECRET)
14481450
expected = "14c00ba6d0dfdedbc86b316847d95b9e6cd46d94"
14491451
self.assertEqual(expected, signature)
14501452

14511453
def test_api_sign_request_sha256(self):
1452-
params = dict(cloud_name="dn6ot3ged", timestamp=1568810420, username="[email protected]")
1453-
signature = api_sign_request(params, "hdcixPpR2iKERPwqvH6sHdK9cyac", cloudinary.utils.SIGNATURE_SHA256)
1454+
params = dict(cloud_name=API_SIGN_REQUEST_CLOUD_NAME, timestamp=1568810420, username="[email protected]")
1455+
signature = api_sign_request(params, API_SIGN_REQUEST_TEST_SECRET, cloudinary.utils.SIGNATURE_SHA256)
14541456
expected = "45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd"
14551457
self.assertEqual(expected, signature)
14561458

1459+
def test_api_sign_request_prevents_parameter_smuggling(self):
1460+
"""Should prevent parameter smuggling via & characters in parameter values"""
1461+
# Test with notification_url containing & characters
1462+
params_with_ampersand = {
1463+
"cloud_name": API_SIGN_REQUEST_CLOUD_NAME,
1464+
"timestamp": 1568810420,
1465+
"notification_url": "https://fake.com/callback?a=1&tags=hello,world"
1466+
}
1467+
1468+
signature_with_ampersand = api_sign_request(params_with_ampersand, API_SIGN_REQUEST_TEST_SECRET)
1469+
1470+
# Test that attempting to smuggle parameters by splitting the notification_url fails
1471+
params_smuggled = {
1472+
"cloud_name": API_SIGN_REQUEST_CLOUD_NAME,
1473+
"timestamp": 1568810420,
1474+
"notification_url": "https://fake.com/callback?a=1",
1475+
"tags": "hello,world" # This would be smuggled if & encoding didn't work
1476+
}
1477+
1478+
signature_smuggled = api_sign_request(params_smuggled, API_SIGN_REQUEST_TEST_SECRET)
1479+
1480+
# The signatures should be different, proving that parameter smuggling is prevented
1481+
self.assertNotEqual(signature_with_ampersand, signature_smuggled,
1482+
"Signatures should be different to prevent parameter smuggling")
1483+
1484+
# Verify the expected signature for the properly encoded case
1485+
expected_signature = "4fdf465dd89451cc1ed8ec5b3e314e8a51695704"
1486+
self.assertEqual(expected_signature, signature_with_ampersand)
1487+
1488+
# Verify the expected signature for the smuggled parameters case
1489+
expected_smuggled_signature = "7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9"
1490+
self.assertEqual(expected_smuggled_signature, signature_smuggled)
1491+
14571492

14581493
if __name__ == '__main__':
14591494
unittest.main()

0 commit comments

Comments
 (0)