5555
5656MOCKED_NOW = 1549533574
5757API_SECRET = 'X7qLTrsES31MzxxkxPPA-pAGGfU'
58+ API_SIGN_REQUEST_TEST_SECRET = "hdcixPpR2iKERPwqvH6sHdK9cyac"
59+ API_SIGN_REQUEST_CLOUD_NAME = "dn6ot3ged"
5860
5961
6062class TestUtils (unittest .TestCase ):
@@ -76,7 +78,8 @@ def setUp(self):
7678 cname = None , # for these tests without actual upload, we ignore cname
7779 api_key = "a" , api_secret = "b" ,
7880 secure_distribution = None ,
79- private_cdn = False )
81+ private_cdn = False ,
82+ signature_version = 2 )
8083
8184 def __test_cloudinary_url (self , public_id = TEST_ID , options = None , expected_url = None , expected_options = None ):
8285 if expected_options is None :
@@ -1443,17 +1446,151 @@ def test_support_long_url_signature(self):
14431446 expected_url = DEFAULT_UPLOAD_PATH + long_signature + "/" + image_name )
14441447
14451448 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" )
1449+ params = dict (
cloud_name = API_SIGN_REQUEST_CLOUD_NAME ,
timestamp = 1568810420 ,
username = "[email protected] " )
1450+ signature = api_sign_request (params , API_SIGN_REQUEST_TEST_SECRET )
14481451 expected = "14c00ba6d0dfdedbc86b316847d95b9e6cd46d94"
14491452 self .assertEqual (expected , signature )
14501453
14511454 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 )
1455+ params = dict (
cloud_name = API_SIGN_REQUEST_CLOUD_NAME ,
timestamp = 1568810420 ,
username = "[email protected] " )
1456+ signature = api_sign_request (params , API_SIGN_REQUEST_TEST_SECRET , cloudinary .utils .SIGNATURE_SHA256 )
14541457 expected = "45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd"
14551458 self .assertEqual (expected , signature )
14561459
1460+ def test_api_sign_request_prevents_parameter_smuggling (self ):
1461+ """Should prevent parameter smuggling via & characters in parameter values"""
1462+ # Test with notification_url containing & characters
1463+ params_with_ampersand = {
1464+ "cloud_name" : API_SIGN_REQUEST_CLOUD_NAME ,
1465+ "timestamp" : 1568810420 ,
1466+ "notification_url" : "https://fake.com/callback?a=1&tags=hello,world"
1467+ }
1468+
1469+ signature_with_ampersand = api_sign_request (params_with_ampersand , API_SIGN_REQUEST_TEST_SECRET )
1470+
1471+ # Test that attempting to smuggle parameters by splitting the notification_url fails
1472+ params_smuggled = {
1473+ "cloud_name" : API_SIGN_REQUEST_CLOUD_NAME ,
1474+ "timestamp" : 1568810420 ,
1475+ "notification_url" : "https://fake.com/callback?a=1" ,
1476+ "tags" : "hello,world" # This would be smuggled if & encoding didn't work
1477+ }
1478+
1479+ signature_smuggled = api_sign_request (params_smuggled , API_SIGN_REQUEST_TEST_SECRET )
1480+
1481+ # The signatures should be different, proving that parameter smuggling is prevented
1482+ self .assertNotEqual (signature_with_ampersand , signature_smuggled ,
1483+ "Signatures should be different to prevent parameter smuggling" )
1484+
1485+ # Verify the expected signature for the properly encoded case
1486+ expected_signature = "4fdf465dd89451cc1ed8ec5b3e314e8a51695704"
1487+ self .assertEqual (expected_signature , signature_with_ampersand )
1488+
1489+ # Verify the expected signature for the smuggled parameters case
1490+ expected_smuggled_signature = "7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9"
1491+ self .assertEqual (expected_smuggled_signature , signature_smuggled )
1492+
1493+ def test_api_sign_request_signature_versions (self ):
1494+ """Should use signature version 1 (without parameter encoding) for backward compatibility"""
1495+ public_id_with_ampersand = 'tests/logo&version=2'
1496+ test_version = 1
1497+
1498+ expected_signature_v1 = api_sign_request (
1499+ {'public_id' : public_id_with_ampersand , 'version' : test_version },
1500+ API_SIGN_REQUEST_TEST_SECRET ,
1501+ cloudinary .utils .SIGNATURE_SHA1 ,
1502+ signature_version = 1
1503+ )
1504+
1505+ expected_signature_v2 = api_sign_request (
1506+ {'public_id' : public_id_with_ampersand , 'version' : test_version },
1507+ API_SIGN_REQUEST_TEST_SECRET ,
1508+ cloudinary .utils .SIGNATURE_SHA1 ,
1509+ signature_version = 2
1510+ )
1511+
1512+ self .assertNotEqual (expected_signature_v1 , expected_signature_v2 )
1513+
1514+ # verify_api_response_signature should use version 1 for backward compatibility
1515+ with patch ('cloudinary.config' , return_value = cloudinary .config (api_secret = API_SIGN_REQUEST_TEST_SECRET )):
1516+ self .assertTrue (
1517+ verify_api_response_signature (
1518+ public_id_with_ampersand ,
1519+ test_version ,
1520+ expected_signature_v1
1521+ )
1522+ )
1523+
1524+ self .assertFalse (
1525+ verify_api_response_signature (
1526+ public_id_with_ampersand ,
1527+ test_version ,
1528+ expected_signature_v2
1529+ )
1530+ )
1531+
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+
14571594
14581595if __name__ == '__main__' :
14591596 unittest .main ()
0 commit comments