Skip to content

Commit 4dc484c

Browse files
Make signature configurable
1 parent bffecf4 commit 4dc484c

File tree

3 files changed

+88
-24
lines changed

3 files changed

+88
-24
lines changed

cloudinary/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def __len__(self):
280280
return len(self.public_id) if self.public_id is not None else 0
281281

282282
def validate(self):
283-
return self.signature == self.get_expected_signature()
283+
return utils.verify_api_response_signature(self.public_id, self.version, self.signature)
284284

285285
def get_prep_value(self):
286286
if None in [self.public_id,
@@ -301,7 +301,7 @@ def get_presigned(self):
301301

302302
def get_expected_signature(self):
303303
return utils.api_sign_request({"public_id": self.public_id, "version": self.version}, config().api_secret,
304-
config().signature_algorithm)
304+
config().signature_algorithm, signature_version=1)
305305

306306
@property
307307
def url(self):

cloudinary/utils.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,10 @@ def sign_request(params, options):
619619
if not api_secret:
620620
raise ValueError("Must supply api_secret")
621621
signature_algorithm = options.get("signature_algorithm", cloudinary.config().signature_algorithm)
622+
signature_version = options.get("signature_version", cloudinary.config().signature_version)
622623

623624
params = cleanup_params(params)
624-
params["signature"] = api_sign_request(params, api_secret, signature_algorithm)
625+
params["signature"] = api_sign_request(params, api_secret, signature_algorithm, signature_version)
625626
params["api_key"] = api_key
626627

627628
return params
@@ -630,7 +631,7 @@ def sign_request(params, options):
630631
def api_sign_request(params_to_sign, api_secret, algorithm=SIGNATURE_SHA1, signature_version=2):
631632
"""
632633
Signs API request parameters using the specified algorithm and signature version.
633-
634+
634635
:param params_to_sign: Parameters to include in the signature
635636
:param api_secret: API secret key
636637
:param algorithm: Signature algorithm (default: SHA1)
@@ -646,7 +647,7 @@ def api_sign_request(params_to_sign, api_secret, algorithm=SIGNATURE_SHA1, signa
646647
def api_string_to_sign(params_to_sign, signature_version=2):
647648
"""
648649
Generates a string to be signed for API requests.
649-
650+
650651
:param params_to_sign: Parameters to include in the signature
651652
:param signature_version: Version of signature algorithm to use:
652653
- Version 1: Original behavior without parameter encoding
@@ -662,19 +663,19 @@ def api_string_to_sign(params_to_sign, signature_version=2):
662663
value = str(v).lower()
663664
else:
664665
value = str(v)
665-
666+
666667
param_string = k + "=" + value
667668
if signature_version >= 2:
668669
param_string = _encode_param(param_string)
669670
params.append(param_string)
670-
671+
671672
return "&".join(sorted(params))
672673

673674

674675
def _encode_param(value):
675676
"""
676677
Encodes a parameter for safe inclusion in URL query strings.
677-
678+
678679
Specifically replaces "&" characters with their percent-encoded equivalent "%26"
679680
to prevent them from being interpreted as parameter separators in URL query strings.
680681
@@ -1613,7 +1614,7 @@ def verify_api_response_signature(public_id, version, signature, algorithm=None)
16131614
:param version: The version of the asset as returned in the API response
16141615
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
16151616
:param algorithm: Name of hashing algorithm to use for calculation of HMACs.
1616-
By default uses `cloudinary.config().signature_algorithm`
1617+
By default, uses `cloudinary.config().signature_algorithm`
16171618
16181619
:return: Boolean result of the validation
16191620
"""
@@ -1641,7 +1642,7 @@ def verify_notification_signature(body, timestamp, signature, valid_for=7200, al
16411642
:param signature: Actual signature. Can be retrieved from the X-Cld-Signature header
16421643
:param valid_for: The desired time in seconds for considering the request valid
16431644
:param algorithm: Name of hashing algorithm to use for calculation of HMACs.
1644-
By default uses `cloudinary.config().signature_algorithm`
1645+
By default, uses `cloudinary.config().signature_algorithm`
16451646
16461647
:return: Boolean result of the validation
16471648
"""

test/test_utils.py

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ def setUp(self):
7878
cname=None, # for these tests without actual upload, we ignore cname
7979
api_key="a", api_secret="b",
8080
secure_distribution=None,
81-
private_cdn=False)
81+
private_cdn=False,
82+
signature_version=2)
8283

8384
def __test_cloudinary_url(self, public_id=TEST_ID, options=None, expected_url=None, expected_options=None):
8485
if expected_options is None:
@@ -1460,31 +1461,31 @@ def test_api_sign_request_prevents_parameter_smuggling(self):
14601461
"""Should prevent parameter smuggling via & characters in parameter values"""
14611462
# Test with notification_url containing & characters
14621463
params_with_ampersand = {
1463-
"cloud_name": API_SIGN_REQUEST_CLOUD_NAME,
1464+
"cloud_name": API_SIGN_REQUEST_CLOUD_NAME,
14641465
"timestamp": 1568810420,
14651466
"notification_url": "https://fake.com/callback?a=1&tags=hello,world"
14661467
}
1467-
1468+
14681469
signature_with_ampersand = api_sign_request(params_with_ampersand, API_SIGN_REQUEST_TEST_SECRET)
1469-
1470+
14701471
# Test that attempting to smuggle parameters by splitting the notification_url fails
14711472
params_smuggled = {
14721473
"cloud_name": API_SIGN_REQUEST_CLOUD_NAME,
1473-
"timestamp": 1568810420,
1474+
"timestamp": 1568810420,
14741475
"notification_url": "https://fake.com/callback?a=1",
14751476
"tags": "hello,world" # This would be smuggled if & encoding didn't work
14761477
}
1477-
1478+
14781479
signature_smuggled = api_sign_request(params_smuggled, API_SIGN_REQUEST_TEST_SECRET)
1479-
1480+
14801481
# The signatures should be different, proving that parameter smuggling is prevented
14811482
self.assertNotEqual(signature_with_ampersand, signature_smuggled,
14821483
"Signatures should be different to prevent parameter smuggling")
1483-
1484+
14841485
# Verify the expected signature for the properly encoded case
14851486
expected_signature = "4fdf465dd89451cc1ed8ec5b3e314e8a51695704"
14861487
self.assertEqual(expected_signature, signature_with_ampersand)
1487-
1488+
14881489
# Verify the expected signature for the smuggled parameters case
14891490
expected_smuggled_signature = "7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9"
14901491
self.assertEqual(expected_smuggled_signature, signature_smuggled)
@@ -1493,23 +1494,23 @@ def test_api_sign_request_signature_versions(self):
14931494
"""Should use signature version 1 (without parameter encoding) for backward compatibility"""
14941495
public_id_with_ampersand = 'tests/logo&version=2'
14951496
test_version = 1
1496-
1497+
14971498
expected_signature_v1 = api_sign_request(
14981499
{'public_id': public_id_with_ampersand, 'version': test_version},
14991500
API_SIGN_REQUEST_TEST_SECRET,
15001501
cloudinary.utils.SIGNATURE_SHA1,
15011502
signature_version=1
15021503
)
1503-
1504+
15041505
expected_signature_v2 = api_sign_request(
15051506
{'public_id': public_id_with_ampersand, 'version': test_version},
15061507
API_SIGN_REQUEST_TEST_SECRET,
15071508
cloudinary.utils.SIGNATURE_SHA1,
15081509
signature_version=2
15091510
)
1510-
1511+
15111512
self.assertNotEqual(expected_signature_v1, expected_signature_v2)
1512-
1513+
15131514
# verify_api_response_signature should use version 1 for backward compatibility
15141515
with patch('cloudinary.config', return_value=cloudinary.config(api_secret=API_SIGN_REQUEST_TEST_SECRET)):
15151516
self.assertTrue(
@@ -1519,7 +1520,7 @@ def test_api_sign_request_signature_versions(self):
15191520
expected_signature_v1
15201521
)
15211522
)
1522-
1523+
15231524
self.assertFalse(
15241525
verify_api_response_signature(
15251526
public_id_with_ampersand,
@@ -1528,6 +1529,68 @@ def test_api_sign_request_signature_versions(self):
15281529
)
15291530
)
15301531

1532+
def test_signature_version_config_support(self):
1533+
"""Should use signature_version from config and produce different signatures for v1 vs v2"""
1534+
# Use params with & characters to show the encoding difference between versions
1535+
params = {'public_id': 'test&image', 'notification_url': 'https://example.com/callback?param=value&other=data'}
1536+
1537+
# Test with config signature_version = 1
1538+
cloudinary.config().signature_version = 1
1539+
1540+
# Test sign_request function uses config values
1541+
options_with_config = {'api_key': 'test_key', 'api_secret': API_SIGN_REQUEST_TEST_SECRET}
1542+
signed_params_config_v1 = cloudinary.utils.sign_request(params.copy(), options_with_config)
1543+
1544+
# Test explicit signature version
1545+
options_explicit_v1 = options_with_config.copy()
1546+
options_explicit_v1['signature_version'] = 1
1547+
signed_params_explicit_v1 = cloudinary.utils.sign_request(params.copy(), options_explicit_v1)
1548+
1549+
self.assertEqual(signed_params_config_v1['signature'], signed_params_explicit_v1['signature'])
1550+
1551+
# Test with config signature_version = 2
1552+
cloudinary.config().signature_version = 2
1553+
1554+
signed_params_config_v2 = cloudinary.utils.sign_request(params.copy(), options_with_config)
1555+
1556+
options_explicit_v2 = options_with_config.copy()
1557+
options_explicit_v2['signature_version'] = 2
1558+
signed_params_explicit_v2 = cloudinary.utils.sign_request(params.copy(), options_explicit_v2)
1559+
1560+
self.assertEqual(signed_params_config_v2['signature'], signed_params_explicit_v2['signature'])
1561+
1562+
# Verify that v1 and v2 actually produce different signatures due to parameter encoding
1563+
self.assertNotEqual(signed_params_config_v1['signature'], signed_params_config_v2['signature'],
1564+
"Signature v1 and v2 should be different for parameters with & characters")
1565+
1566+
def test_sign_request_with_signature_version(self):
1567+
"""Should support signature_version parameter in sign_request function"""
1568+
params = {'public_id': 'test_image', 'version': 1234}
1569+
options = {'api_key': 'test_key', 'api_secret': API_SIGN_REQUEST_TEST_SECRET}
1570+
1571+
# Test with signature_version in options
1572+
options_v1 = options.copy()
1573+
options_v1['signature_version'] = 1
1574+
signed_params_v1 = cloudinary.utils.sign_request(params.copy(), options_v1)
1575+
1576+
options_v2 = options.copy()
1577+
options_v2['signature_version'] = 2
1578+
signed_params_v2 = cloudinary.utils.sign_request(params.copy(), options_v2)
1579+
1580+
# The signatures should be different for different versions (for params with & characters)
1581+
# For these simple params without & they might be the same, but let's test the structure
1582+
self.assertIn('signature', signed_params_v1)
1583+
self.assertIn('signature', signed_params_v2)
1584+
self.assertIn('api_key', signed_params_v1)
1585+
self.assertIn('api_key', signed_params_v2)
1586+
1587+
# Test that signature_version is passed through correctly
1588+
expected_sig_v1 = api_sign_request(params, API_SIGN_REQUEST_TEST_SECRET, cloudinary.utils.SIGNATURE_SHA1, 1)
1589+
expected_sig_v2 = api_sign_request(params, API_SIGN_REQUEST_TEST_SECRET, cloudinary.utils.SIGNATURE_SHA1, 2)
1590+
1591+
self.assertEqual(signed_params_v1['signature'], expected_sig_v1)
1592+
self.assertEqual(signed_params_v2['signature'], expected_sig_v2)
1593+
15311594

15321595
if __name__ == '__main__':
15331596
unittest.main()

0 commit comments

Comments
 (0)